Distru API
Stay Up To Date
It is very important that you sign up for our email list. It is the only way we announce breaking changes to the Distru public API — if you are not on the list you will not be warned before a change ships, and your integration may break. We also use it to announce new endpoints and features as they launch, so you can take advantage of them.
Overview
Distru's public API allows you to easily access and manipulate your data in our system automatically.
- Integrate your supply chain with the rest of your business Build your own custom connectors that support your most important workflows.
- Extend your reports Take data from Distru and pipe it into other systems for a unified view of your business.
- Maximize your efficiency Automate tasks based on changes in your distribution, manufacturing, and sales.
OpenAPI Specification
Download the OpenAPI 3.0 specification to generate a typed client in your language or explore the API in your own tools. Import it into Postman, Insomnia, or any OpenAPI code generator to scaffold your integration.
Base URL
All API requests should be made to the base URL: https://app.distru.com
Data Formats
Field types at a glance
{
"id": "8f3c9d2e-1a4b-4c7d-9e0f-2b6a1c3d4e5f",
"order_number": "SO-0001",
"status": "PENDING",
"total": "60.00",
"quantity": "5",
"order_datetime": "2025-05-04T04:40:21.817570Z",
"delivered_datetime": null
}
The API uses consistent types across every endpoint. Build your integration against these rules, not against the shape of any single response.
| Type | Format |
|---|---|
| IDs | UUID strings, e.g. "8f3c9d2e-1a4b-4c7d-9e0f-2b6a1c3d4e5f". Treat them as opaque — do not parse or assume ordering. |
| Numbers | All quantities, prices, and monetary amounts are strings, e.g. "60.00", "5". This preserves exact decimal precision that JSON floats cannot. Parse them with a decimal library, never a float. A few fields still return JSON numbers today; we will make them strings in the next API version, so parse defensively. |
| Datetimes | UTC, ISO-8601 with microseconds and a Z suffix: YYYY-MM-DDTHH:MM:SS.MSZ. Datetime fields end in _datetime (e.g. order_datetime, delivered_datetime). |
| Enums | Stable uppercase string tokens, e.g. "PENDING", "COMPLETED". A few enums are not yet uppercase; we will normalize them in the next API version. New values may be added over time — handle unknown tokens gracefully. |
| Booleans | JSON true / false. |
| Null | An absent or unset value is null, not omitted. Optional fields are always present in the response. |
Conventions
- Money and quantities are strings.
"total": "60.00"is a string, not the number60.0. Never round-trip these through a float — use a decimal type in your language. - Unknown fields may appear at any time. We add fields to responses without notice and consider that non-breaking. Your parser must ignore fields it does not recognize.
- Custom fields appear under
custom_data, keyed by the custom field's id. - IDs are opaque. Their format may change; match on equality only.
Getting Started
To integrate with Distru, you'll need to contact a representative that will can enable your account's API access. From there, have a look at the following information on how to authenticate with Distru.
Sandbox
We offer a sandbox environment where you can build and test your integration against the API without touching your production data. Reach out to your Customer Support Representative to request access.
Authentication
An admin user can generate API keys in the Distru app by following these steps.
Steps:
- Log in to Distru with your admin account.
- Navigate to the Settings page from the left menu.
- Click on Distru API under the Integrations section.
- Use the Create API Key option to generate your API token.
Using your API token
Example authenticated request
GET /public/v1/orders?page[number]=1
content-type: application/json
accept: application/json
authorization: Bearer YOUR_API_TOKEN
Every request must include your token in the Authorization header using the
Bearer scheme, along with JSON content-type and accept headers:
| Header | Value |
|---|---|
Authorization |
Bearer YOUR_API_TOKEN |
Content-Type |
application/json |
Accept |
application/json |
Treat your token like a password: it carries the same permissions as the admin who created it. Keep it server-side and never expose it in client-side code.
Handling Errors
Every error response uses this envelope
{
"errors": [
{
"message": "Quantity must be greater than 0",
"pointer": ["items", 0, "quantity"]
},
{
"message": "Not Found",
"pointer": ["company_id"]
}
]
}
Every endpoint reports failures the same way: a JSON body with a single errors
array, where each entry describes one problem. You only need to write one error
parser for the entire API.
| Field | Description |
|---|---|
message |
A human-readable description of the problem. |
pointer |
The path to the offending part of your request: field names and array indices, e.g. ["items", 0, "quantity"] is the quantity of the first entry in items. ["base"] means the error applies to the request as a whole. |
Use pointer — not the text of message — to react to errors programmatically.
Messages may be reworded at any time without notice; pointers are stable.
Some responses include additional section and context fields on each error.
These are deprecated — ignore them, and do not build logic on them.
Status codes
| Status | Meaning |
|---|---|
400 |
The request was invalid: a malformed or missing field, a bad filter value, or a business rule that rejects it. The errors array says what and where. |
401 |
The API token is missing or invalid. |
403 |
The token is valid but lacks the permission the endpoint requires. |
404 |
No record with that id exists for your company. |
429 |
Rate limit exceeded (PDF endpoints). Back off and retry later. |
These are the only error statuses the API returns. A 5xx response is never
something your request caused: it is a bug on our end, and we'd appreciate a
report to support with the request you sent (minus your token).
Rate Limiting
A 429 response includes a
Retry-Afterheader (seconds to wait)
HTTP/1.1 429 Too Many Requests
retry-after: 42
content-type: application/json
{
"errors": [
{
"message": "PDF download rate limit exceeded (20/minute, 1000/day per account, aggregated across all PDF endpoints). Retry after the Retry-After period.",
"pointer": ["base"]
}
]
}
Only PDF download endpoints (any path ending in /pdf) are rate limited. All
other endpoints are currently unlimited, though we may add limits in the future.
The PDF limit is per account, aggregated across every PDF endpoint — not per endpoint:
| Window | Limit |
|---|---|
| Per minute | 20 successful downloads |
| Per day | 1,000 successful downloads |
Only successful (2xx) downloads count toward the limit. Requests that are
rate limited (429), forbidden, or fail to render do not consume quota.
When you exceed a limit, the API returns 429 Too Many Requests with a
Retry-After header giving the number of seconds to wait before retrying. The
window is sliding, so Retry-After reflects when the oldest counted request ages
out — honor it rather than retrying on a fixed schedule.
Webhooks
Distru can push changes to your systems in real time via webhooks. A webhook fires whenever a record is created, edited, or deleted. We support webhooks for:
- Sales Orders
- Purchase Orders
- Assemblies
- Invoices
- Companies
- Returns
- Products
To set up webhooks, reach out to Customer Support to find out more.
Webhook payload shape
Every webhook is a JSON POST with three top-level fields:
| Field | Description |
|---|---|
type |
The entity type, e.g. ORDER, PURCHASE, INVOICE, PRODUCT. |
id |
The public id of the entity the webhook is about (the same value as object.id). |
object |
The full entity, in the same structure the GET /public/v1/<type>/<id> endpoint returns. |
object is the current version of the entity, fetched fresh when the
webhook is sent — not a snapshot from when the change was committed. If other
changes were committed in between, they are reflected. On a hard-delete event
the entity no longer exists, so object is null; use type and id.
To keep integrations simple, object matches the corresponding GET endpoint
exactly, so you can parse it with the same code you already use for that
endpoint.
A sales order is created or edited (
objectis the full order, same asGET /public/v1/orders/:id)
{
"type": "ORDER",
"id": "8f3c9d2e-1a4b-4c7d-9e0f-2b6a1c3d4e5f",
"object": {
"id": "8f3c9d2e-1a4b-4c7d-9e0f-2b6a1c3d4e5f",
"order_number": "SO-0001",
"status": "PENDING",
"total": "60.00",
"order_datetime": "2025-05-04T04:40:21.817570Z",
"company": {"id": "b7e2…", "name": "Acme Dispensary"},
"items": [
{
"id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"product_id": "0c9e7f11-2a33-4b55-8c77-9d0e1f2a3b4c",
"quantity": "5",
"price": "10.00"
}
],
"charges": []
}
}
A sales order is hard-deleted (
objectis null)
{
"type": "ORDER",
"id": "8f3c9d2e-1a4b-4c7d-9e0f-2b6a1c3d4e5f",
"object": null
}
Objects above are trimmed for readability; a real object includes the full set
of fields returned by the matching GET endpoint.
Webhook nested entities
A change to a nested record triggers a webhook at its parent level —
never as its own webhook — and object is always the full parent entity.
The nested records that trigger each parent are:
| Parent | Nested records |
|---|---|
| Sales Orders | items, charges |
| Purchase Orders | items, charges |
| Invoices | items, charges, payments |
| Assemblies | inputs, outputs, costs |
| Returns | items |
| Companies | the related company, its locations, its licenses, and its contacts (each with its profile) |
| Products | bills of materials (and each bill of materials' inputs and costs) |
For example, adding an order item to a sales order does not send an "order
item" webhook — it sends a Sales Order webhook whose object is the full
order, including the new item.
Note: inventory changes (adding or removing quantity) do not currently trigger Product webhooks.
Webhook signing & verification
The
x-distru-signatureheader
x-distru-signature: sha256=<hex digest>
Node.js verification example
const crypto = require('crypto');
function isValid(rawBody, signatureHeader, secret) {
const expected =
'sha256=' +
crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}
When Customer Support sets up your webhook, they provide a signing secret.
Distru signs every request so you can confirm it came from us and was not
tampered with. Each request includes an x-distru-signature header.
The digest is an HMAC-SHA256 of the raw request body using your signing
secret. To verify a request:
- Read the raw request body exactly as received — do not re-serialize the JSON, since key order must match.
- Compute
HMAC-SHA256(raw_body, your_secret)and hex-encode it. - Compare
sha256=<your digest>to thex-distru-signatureheader using a constant-time comparison.
Webhook retries
If your endpoint is unreachable or returns a non-2xx status, Distru retries
delivery with an increasing delay between attempts (up to ~10 attempts over
roughly 4 hours) before giving up.
Pagination
Endpoint query with page number example
https://app.distru.com/public/v1/products?page[number]=1
cURL example
curl --location --globoff 'https://app.distru.com/public/v1/products?page[number]=1' \
--header 'Authorization: Bearer ********* API KEY HERE *********'
Next page URL in the response body (if a next page exists)
"next_page": "https://app.distru.com/public/v1/products?page[number]=2"
Use the page[number]= query parameter to request a specific page. When another
page is available, the response body includes a next_page URL you can follow.
Do not assume a fixed number of items per page. Distru may change the page size of
any GET endpoint at any time without notice, so always paginate by following the
next_page URL until it is absent rather than relying on a specific page size.
Filtering by datetime parameters
On or after May 4th, 2025
products?updated_datetime=2025-05-04T04:40:21.817570Z,
On or before May 4th, 2025
products?updated_datetime=,2025-05-04T04:40:21.817570Z
Between May 4th and Sept 18th, 2025 (inclusive)
products?updated_datetime=2025-05-04T04:40:21.817570Z,2025-09-18T16:27:44.946871Z
Datetimes use the format YYYY-MM-DDTHH:MM:SS.MSZ.
The comma position controls the direction of the filter. All bounds are inclusive: a record matching the exact datetime in the query is returned.
- Comma after the datetime = records on or after that datetime.
- Comma before the datetime = records on or before that datetime.
- Datetimes on both sides of the comma = records between the two datetimes, inclusive of both ends.
Sparse Updates
Create a cost type, then rename it without touching anything else
// POST /public/v1/cost-types — create
{ "name": "Freight", "cost_per_unit": "5.00", "active": true }
// POST /public/v1/cost-types — update: only the name changes; cost_per_unit and active are kept
{ "id": "8f3c…", "name": "Inbound Freight" }
// clear the description, leave everything else as-is
{ "id": "8f3c…", "description": null }
Every write endpoint is an upsert: one POST both creates and updates. Omit the
id to create a new record; include an existing id to update that record. The URL
and request shape are identical either way.
On an update you send only the fields you want to change:
- Omit a field → left unchanged.
- Send a field with a value → set to that value.
- Send a field as
null→ set to null. A field that is required, or that cannot change once set, rejectsnullwith a400— the same as omitting a required field on create.
A create is not sparse: it must include every required field, because there is no existing record to fall back to.
Nested objects
Update a price tier's conditions — set one filter, clear another, leave the rest
// POST /public/v1/price-tiers
{
"id": "8f3c…",
"conditions": {
"one_of_product_ids": ["0c9e…"], // set this filter
"one_of_company_ids": [] // clear this filter
// every other condition is left untouched
}
}
A nested object is sparse the same way: send only the keys you want to change and
omit the rest. To clear a single key without touching the others, send it as null;
a list-valued key (like the condition filters below) is also cleared by an empty
array [].
Collections (line items, charges, …)
Add one charge, keep an existing one, and drop any others — while leaving line items untouched
// POST /public/v1/purchases
{
"id": "8f3c…",
"charges": [
{"id": "a1b2…"}, // keep as-is
{
"name": "Fuel Surcharge",
"price": "5.00",
"type": "CHARGE",
"unit_type": "PRICE"
} // add
// any other existing charge is deleted
]
// "items" omitted entirely → the line items are left untouched
}
A collection — such as a purchase's items and charges — is optional on
update: omit the whole field to leave its existing entries exactly as they are.
When you do send a collection, it is the complete set for the record:
- An entry with an existing
idis patched — merged onto the stored entry, so you can change one field and omit the rest. - An entry without an
idis added as a new entry. - Any existing entry whose
idyou do not include is deleted. - An empty array clears the whole collection (or fails if clearing is not allowed).
Because a collection deletes by omission, you cannot separately mark a single entry for deletion — you drop it by leaving it out of a sent collection.
Assemblies are different
The assembly upsert (POST /public/v1/assemblies) does not follow the
omit-to-delete model above. It is driven by an explicit per-row action
(CREATE, UPDATE, or DELETE) on the assembly and its nested rows. Before
integrating against it, read the assemblies endpoint docs
specifically — the rules in this section do not apply.
All other upsert endpoints follow the rules above; where one differs, its own endpoint docs call it out, so check them before you rely on this behavior.
Endpoints
Assembly
Delete an assembly
Success scenario
DELETE /public/v1/assemblies/da288ce6-6cd6-4703-8cc1-d3155b729d42
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzMsImlhdCI6MTc4NzU4NzI3MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiMDAxODU1ZWItMWM0Zi00YjMxLThiYWUtNDQyMDJiMTg5ODUxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzE0NSIsInR5cCI6ImFjY2VzcyJ9.D-shSF8ZZCxFJEedGeiqGTKZV-nL-2_exlCTf0y6z8o
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 889f2b8eba36e749f14c400e7bce22f2-d4a060f978c2284c-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cf914631d8868f824545ba38e52cf1ab-aa618aa77fb9cfbe-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Some assemblies cannot be deleted; each of these is refused with a 400 and nothing is changed:
• 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.
• 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.
• A system-generated assembly (creation_source SALES_ORDER, SPLIT_PACKAGE, or LAB_TESTING) — only MANUALLY_CREATED assemblies can be deleted through the API.
• A BioTrack assembly.
• An assembly another request is mutating at that same moment (transient — retry).
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.
Other 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).
Required permission: assemblies_permissions_delete.
Request
DELETE /public/v1/assemblies/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get an assembly
Success scenario
GET /public/v1/assemblies/da288ce6-6cd6-4703-8cc1-d3155b729d42
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzMsImlhdCI6MTc4NzU4NzI3MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiMDAxODU1ZWItMWM0Zi00YjMxLThiYWUtNDQyMDJiMTg5ODUxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzE0NSIsInR5cCI6ImFjY2VzcyJ9.D-shSF8ZZCxFJEedGeiqGTKZV-nL-2_exlCTf0y6z8o
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 889f2b8eba36e749f14c400e7bce22f2-d4a060f978c2284c-0
{
"data": {
"assembly_number": "AS-0000001",
"completion_datetime": "2026-08-24T16:01:13.799039Z",
"compliance_type": "NONE",
"creation_source": "MANUALLY_CREATED",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-3129@example.com",
"full_name": "FirstName6356 LastName6357",
"id": "00000000-0000-0000-0000-000000000c49",
"inserted_datetime": "2026-08-24T16:01:13.746083Z",
"role": {
"id": "00000000-0000-0000-0000-000000000c8c",
"name": "Admin 3211"
}
},
"custom_data": [],
"description": null,
"estimated_start_date": null,
"estimated_start_datetime": null,
"estimated_work_hours": null,
"estimated_work_minutes": null,
"fulfilled": true,
"id": "da288ce6-6cd6-4703-8cc1-d3155b729d42",
"inserted_datetime": "2026-08-24T16:01:13.799039Z",
"is_metrc_processing_job": false,
"license": null,
"metrc_processing_job": null,
"metrc_processing_job_id": null,
"metrc_processing_job_name": null,
"metrc_processing_job_notes": null,
"metrc_processing_job_type_id": null,
"outputs": [
{
"additional_costs": [],
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000253",
"name": "B1892"
},
"batch_number": null,
"bins": [],
"compliance_label": null,
"compliance_quantity": null,
"copy_custom_data_from_input": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"costs": [],
"expiration_date": null,
"expiration_datetime": null,
"id": "ba83fd6d-f1d4-4d2c-9a44-b8eed590f89f",
"ingredients": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000253",
"name": "B1892"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "2ebab8c6-933f-4ede-9719-4233778c7d2b",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-0000000008b2",
"id": "00000000-0000-0000-0000-000000000264",
"license_id": null,
"name": "Place 611"
},
"package": null,
"product": {
"id": "ace5db7b-468c-4d89-8876-f59dd7ae59a2",
"name": "Product 1888",
"sku": "sku 1889",
"updated_datetime": "2026-08-24T16:01:13.761340Z"
},
"quantity": "2",
"status": "COMPLETED",
"total_cost_actual": null,
"total_cost_default": null
}
],
"inputs": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000253",
"name": "B1892"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "2ebab8c6-933f-4ede-9719-4233778c7d2b",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-0000000008b2",
"id": "00000000-0000-0000-0000-000000000264",
"license_id": null,
"name": "Place 611"
},
"package": null,
"product": {
"id": "ace5db7b-468c-4d89-8876-f59dd7ae59a2",
"name": "Product 1888",
"sku": "sku 1889",
"updated_datetime": "2026-08-24T16:01:13.761340Z"
},
"quantity": "2",
"status": "COMPLETED",
"total_cost_actual": null,
"total_cost_default": null
}
],
"is_donation": false,
"is_finished_good": false,
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": null,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-0000000008b2",
"id": "00000000-0000-0000-0000-000000000264",
"license_id": null,
"name": "Place 611"
},
"metrc_item_id": null,
"metrc_location_id": null,
"metrc_notes": null,
"metrc_production_batch_number": null,
"package": null,
"package_date": null,
"package_datetime": null,
"package_unit_type": null,
"product": {
"id": "ace5db7b-468c-4d89-8876-f59dd7ae59a2",
"name": "Product 1888",
"sku": "sku 1889",
"updated_datetime": "2026-08-24T16:01:13.761340Z"
},
"quantity": "2",
"status": "COMPLETED",
"total_cost_actual": null,
"total_cost_default": null,
"use_same_item": false
}
],
"owner_id": "00000000-0000-0000-0000-000000000c49",
"status": "COMPLETED",
"updated_datetime": "2026-08-24T16:01:13.799039Z",
"waste_count_quantity": null,
"waste_count_unit_name": null,
"waste_volume_quantity": null,
"waste_volume_unit_name": null,
"waste_weight_quantity": null,
"waste_weight_unit_name": null
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 57bedbc3423f5f80789cbfdc9b748faa-fdbabc0ac67b90ea-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get a single assembly by ID, with its full outputs, inputs, and costs.
Like the list endpoint, this reflects eventually consistent data — an assembly you just created or updated may take up to a second to reflect its latest state here. Returns 404 if no assembly with that ID exists in your company or your team restrictions hide it.
Required permission: assemblies_permissions_view.
Request
GET /public/v1/assemblies/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The ID of the assembly to fetch. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single assembly | AssemblyResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get assemblies
Success scenario
GET /public/v1/assemblies?creation_source=MANUALLY_CREATED&page[number]=1
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiNWZmZWM5OWUtNGRjOS00ZWFjLWI3M2ItNWM4ZDNkNTdmZDFjIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTQ2MSIsInR5cCI6ImFjY2VzcyJ9.l8iitWZHmCTdLTJ-AdfKI781UBASEq1CN549NIODOR0
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 71f8b8ac76b82c76d55b0435f8c05ca0-125a3f933bfe8602-0
{
"data": [
{
"assembly_number": "AS-0000001",
"completion_datetime": "2026-08-24T16:01:08.117123Z",
"compliance_type": "NONE",
"creation_source": "MANUALLY_CREATED",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1458@example.com",
"full_name": "FirstName2956 LastName2957",
"id": "00000000-0000-0000-0000-0000000005b5",
"inserted_datetime": "2026-08-24T16:01:07.959961Z",
"role": {
"id": "00000000-0000-0000-0000-0000000005d7",
"name": "Admin 1494"
}
},
"custom_data": [
{
"id": 37,
"name": "Custom Field 30",
"value": "Custom Field Value"
}
],
"description": null,
"estimated_start_date": "2024-01-02T03:04:05.000000Z",
"estimated_start_datetime": "2024-01-02T03:04:05.000000Z",
"estimated_work_hours": 1,
"estimated_work_minutes": 5,
"fulfilled": true,
"id": "d2dfd158-eb35-4cbe-bb62-9f0957c98a96",
"inserted_datetime": "2026-08-24T16:01:08.117123Z",
"is_metrc_processing_job": false,
"license": null,
"metrc_processing_job": null,
"metrc_processing_job_id": null,
"metrc_processing_job_name": null,
"metrc_processing_job_notes": null,
"metrc_processing_job_type_id": null,
"outputs": [
{
"additional_costs": [
{
"cost_per_unit": "-1",
"description": null,
"id": "69b1b7fe-eb8f-4ae3-884e-989ece3a9c47",
"name": "CostType 42",
"quantity": "1",
"total_cost_actual": "-1",
"total_cost_default": "0",
"unit_type": {
"id": "00000000-0000-0000-0000-0000000036c8",
"name": "Unit Type 55"
}
}
],
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000009b",
"name": "B585"
},
"batch_number": null,
"bins": [],
"compliance_label": null,
"compliance_quantity": null,
"copy_custom_data_from_input": null,
"cost_per_unit": "-0.3",
"cost_per_unit_default": "0.5",
"costs": [
{
"cost_per_unit": "-1",
"description": null,
"id": "69b1b7fe-eb8f-4ae3-884e-989ece3a9c47",
"name": "CostType 42",
"quantity": "1",
"total_cost_actual": "-1",
"total_cost_default": "0",
"unit_type": {
"id": "00000000-0000-0000-0000-0000000036c8",
"name": "Unit Type 55"
}
}
],
"expiration_date": null,
"expiration_datetime": null,
"id": "d0b8ea27-7905-46f3-bd94-8c4bb0bb8073",
"ingredients": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000009b",
"name": "B585"
},
"compliance_quantity": null,
"cost_per_unit": "0.2",
"cost_per_unit_default": "1",
"id": "b11e800f-0b61-44fc-ab3a-46ddcfb03cd6",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000454",
"id": "00000000-0000-0000-0000-00000000012b",
"license_id": null,
"name": "Place 298"
},
"package": null,
"product": {
"id": "d9071deb-47a7-4ae6-ae24-801a4422f978",
"name": "Product 577",
"sku": "sku 578",
"updated_datetime": "2026-08-24T16:01:08.002478Z"
},
"quantity": "2",
"status": "COMPLETED",
"total_cost_actual": "0.4",
"total_cost_default": "2"
}
],
"inputs": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000009b",
"name": "B585"
},
"compliance_quantity": null,
"cost_per_unit": "0.2",
"cost_per_unit_default": "1",
"id": "b11e800f-0b61-44fc-ab3a-46ddcfb03cd6",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000454",
"id": "00000000-0000-0000-0000-00000000012b",
"license_id": null,
"name": "Place 298"
},
"package": null,
"product": {
"id": "d9071deb-47a7-4ae6-ae24-801a4422f978",
"name": "Product 577",
"sku": "sku 578",
"updated_datetime": "2026-08-24T16:01:08.002478Z"
},
"quantity": "2",
"status": "COMPLETED",
"total_cost_actual": "0.4",
"total_cost_default": "2"
}
],
"is_donation": false,
"is_finished_good": false,
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": null,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000454",
"id": "00000000-0000-0000-0000-00000000012b",
"license_id": null,
"name": "Place 298"
},
"metrc_item_id": null,
"metrc_location_id": null,
"metrc_notes": null,
"metrc_production_batch_number": null,
"package": null,
"package_date": null,
"package_datetime": null,
"package_unit_type": null,
"product": {
"id": "d9071deb-47a7-4ae6-ae24-801a4422f978",
"name": "Product 577",
"sku": "sku 578",
"updated_datetime": "2026-08-24T16:01:08.002478Z"
},
"quantity": "2",
"status": "COMPLETED",
"total_cost_actual": "-0.6",
"total_cost_default": "1",
"use_same_item": false
}
],
"owner_id": "00000000-0000-0000-0000-0000000005b5",
"status": "COMPLETED",
"updated_datetime": "2026-08-24T16:01:08.117123Z",
"waste_count_quantity": null,
"waste_count_unit_name": null,
"waste_volume_quantity": null,
"waste_volume_unit_name": null,
"waste_weight_quantity": null,
"waste_weight_unit_name": null
}
],
"next_page": null
}
Error scenario: invalid status filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: be55725f2d373eeca99b0b224876aa5b-668c0a3d2d96c896-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"status"
],
"section": "query"
}
]
}
Get a page of assemblies, each with its full outputs, inputs, and costs, ordered oldest to newest by their last modified date. All filters below are ANDed together, and results are paginated.
Nested input_* and output_* filters match against an assembly's inputs and outputs. When you
combine several filters for the same side, a single line must satisfy all of them: e.g.
input_product_ids + input_batch_ids keeps only assemblies that have one input matching both.
Input and output filters combine across sides too — an assembly must have a matching input AND a
matching output to be returned.
This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses — an assembly you just created or updated (including via the upsert or split-package endpoints) may not appear here for up to a second.
Required permission: assemblies_permissions_view. Results are scoped to your company and further
filtered to only the assemblies the authenticated user's team restrictions allow them to see, so a
restricted key may see fewer rows than exist.
Request
GET /public/v1/assemblies
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| completion_datetime | 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. |
query | string | false | ?completion_datetime=2022-07-01T00:00:00Z,2022-07-31T00:00:00Z | |
| creation_source | Filter to assemblies that originated a given way, matching exactly one value.
MANUALLY_CREATED SALES_ORDER SPLIT_PACKAGE LAB_TESTING |
query | string | false | ||
| custom_data | 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. |
query | object | false | ?custom_data[101]=Blue&custom_data[102]=Wholesale | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| input_batch_batch_numbers | 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. | query | array | false | ?input_batch_batch_numbers[]=BATCH-001 | |
| input_batch_ids | 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. | query | array | false | ?input_batch_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| input_package_batch_numbers | 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. | query | array | false | ?input_package_batch_numbers[]=BATCH-001 | |
| input_package_compliance_labels | 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. | query | array | false | ?input_package_compliance_labels[]=1A4000000000000000000123 | |
| input_package_ids | 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. | query | array | false | ?input_package_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| input_product_brand_ids | 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. | query | array | false | ?input_product_brand_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| input_product_category_ids | 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. | query | array | false | ?input_product_category_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| input_product_group_ids | 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. | query | array | false | ?input_product_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| input_product_ids | 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. | query | array | false | ?input_product_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| input_product_skus | 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. | query | array | false | ?input_product_skus[]=SKU-123 | |
| input_product_strain_ids | 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. | query | array | false | ?input_product_strain_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| input_product_subcategory_ids | 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. | query | array | false | ?input_product_subcategory_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| input_product_tag_ids | 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. | query | array | false | ?input_product_tag_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| input_product_vendor_ids | 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. | query | array | false | ?input_product_vendor_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-01T00:00:00Z,2022-07-31T00:00:00Z | |
| license_ids | 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. |
query | array | false | ?license_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| license_number | 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. | query | string | false | ?license_number=C11-0000123-LIC | |
| output_batch_batch_numbers | 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. | query | array | false | ?output_batch_batch_numbers[]=BATCH-001 | |
| output_batch_ids | 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. | query | array | false | ?output_batch_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| output_package_batch_numbers | 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. | query | array | false | ?output_package_batch_numbers[]=BATCH-001 | |
| output_package_compliance_labels | 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. | query | array | false | ?output_package_compliance_labels[]=1A4000000000000000000123 | |
| output_package_ids | 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. | query | array | false | ?output_package_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| output_product_brand_ids | 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. | query | array | false | ?output_product_brand_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| output_product_category_ids | 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. | query | array | false | ?output_product_category_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| output_product_group_ids | 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. | query | array | false | ?output_product_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| output_product_ids | 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. | query | array | false | ?output_product_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| output_product_skus | 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. | query | array | false | ?output_product_skus[]=SKU-123 | |
| output_product_strain_ids | 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. | query | array | false | ?output_product_strain_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| output_product_subcategory_ids | 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. | query | array | false | ?output_product_subcategory_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| output_product_tag_ids | 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. | query | array | false | ?output_product_tag_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| output_product_vendor_ids | 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. | query | array | false | ?output_product_vendor_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| status | Filter by assembly status, matching exactly one value.
PENDING COMPLETED |
query | string | false | ||
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-01T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of assemblies | Assemblies |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Split a package
Success scenario
POST /public/v1/assemblies/split_package
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNzYwZmMwYjItODAzNS00MTA3LWI1NTAtMTYzNDRlODY5ODkzIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDQyIiwidHlwIjoiYWNjZXNzIn0.MqsPaLREwun3p197eNK95EHcv5zRMM-eMsSXM1fNwOg
{
"outputs": [
{
"batch_number": "OUT-1",
"bin_ids": [
"e42ad20e-d0bc-4a3c-95dc-16cba891ef93"
],
"compliance_label": "1A4010200001234000000001",
"costs": [
{
"cost_type_id": "00000000-0000-0000-0000-000000000019",
"quantity": 2
}
],
"expiration_date": "2027-08-19",
"input_compliance_quantity": 3,
"location_id": "00000000-0000-0000-0000-00000000004c",
"metrc_notes": "eighths",
"output_compliance_quantity": 3,
"product_id": "07fce643-63bf-498c-a2c1-7eee6f192210",
"use_same_item": true
},
{
"compliance_label": "1A4010200001234000000002",
"input_compliance_quantity": 2,
"location_id": "00000000-0000-0000-0000-00000000004c",
"metrc_item_id": 1000001,
"output_compliance_quantity": 2,
"product_id": "07fce643-63bf-498c-a2c1-7eee6f192210"
}
],
"source_package_id": "00000000-0000-0000-0000-000000000004"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: dec33f4de3d1d030e7a49f5540bd520e-6043e45529b3e5fc-0
{
"data": {
"assembly_number": "AS-0000001",
"completion_datetime": "2026-08-24T16:01:04.779933Z",
"compliance_type": "METRC",
"creation_source": "SPLIT_PACKAGE",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-440@example.com",
"full_name": "FirstName902 LastName903",
"id": "00000000-0000-0000-0000-0000000001ba",
"inserted_datetime": "2026-08-24T16:01:04.348430Z",
"role": {
"id": "00000000-0000-0000-0000-0000000001c5",
"name": "Admin 452"
}
},
"custom_data": [],
"description": null,
"estimated_start_date": null,
"estimated_start_datetime": null,
"estimated_work_hours": null,
"estimated_work_minutes": null,
"fulfilled": true,
"id": "82ab1326-7ebe-479d-8405-0bace683715b",
"inserted_datetime": "2026-08-24T16:01:04.779933Z",
"is_metrc_processing_job": false,
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:04.368780Z",
"id": "00000000-0000-0000-0000-00000000000b",
"inserted_datetime": "2026-08-24T16:01:04.368836Z",
"issue_datetime": "2026-08-24T16:01:04.368779Z",
"license_number": "CDPH-00000012",
"license_type": "Type 8 Testing"
},
"metrc_processing_job": null,
"metrc_processing_job_id": null,
"metrc_processing_job_name": null,
"metrc_processing_job_notes": null,
"metrc_processing_job_type_id": null,
"outputs": [
{
"additional_costs": [
{
"cost_per_unit": "2",
"description": null,
"id": "df4277a1-0c64-4d28-b292-0c892e4fdaf1",
"name": "CostType 23",
"quantity": "2",
"total_cost_actual": "4",
"total_cost_default": "0",
"unit_type": {
"id": "00000000-0000-0000-0000-000000001521",
"name": "Unit Type 32"
}
}
],
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000002f",
"name": "B161"
},
"batch_number": "OUT-1",
"bins": [
{
"id": "e42ad20e-d0bc-4a3c-95dc-16cba891ef93",
"name": "Bin 3"
}
],
"compliance_label": "1A4010200001234000000001",
"compliance_quantity": "3",
"copy_custom_data_from_input": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"costs": [
{
"cost_per_unit": "2",
"description": null,
"id": "df4277a1-0c64-4d28-b292-0c892e4fdaf1",
"name": "CostType 23",
"quantity": "2",
"total_cost_actual": "4",
"total_cost_default": "0",
"unit_type": {
"id": "00000000-0000-0000-0000-000000001521",
"name": "Unit Type 32"
}
}
],
"expiration_date": "2027-08-19T00:00:00.000000Z",
"expiration_datetime": "2027-08-19T00:00:00.000000Z",
"id": "a293eac3-f12d-4fa5-8bb9-c50b65b722f1",
"ingredients": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000030",
"name": "B162"
},
"compliance_quantity": "3",
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "be190672-b4f2-4e9c-abdc-e857f02337da",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000171",
"id": "00000000-0000-0000-0000-00000000004c",
"license_id": "00000000-0000-0000-0000-00000000000b",
"name": "Place 75"
},
"package": {
"batch_number": "SRC-1",
"compliance_label": "ABCDEF012345670000000006",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-000000000004",
"license_id": "00000000-0000-0000-0000-00000000000b",
"location_id": "00000000-0000-0000-0000-00000000004c",
"metrc_id": 6,
"metrc_label": "ABCDEF012345670000000006",
"quantity": "5.000000000",
"quantity_active": "5.000000000",
"status": "active"
},
"product": {
"id": "07fce643-63bf-498c-a2c1-7eee6f192210",
"name": "Product 159",
"sku": "sku 160",
"updated_datetime": "2026-08-24T16:01:04.414458Z"
},
"quantity": "85.048477632",
"status": "COMPLETED",
"total_cost_actual": null,
"total_cost_default": null
}
],
"inputs": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000030",
"name": "B162"
},
"compliance_quantity": "3",
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "be190672-b4f2-4e9c-abdc-e857f02337da",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000171",
"id": "00000000-0000-0000-0000-00000000004c",
"license_id": "00000000-0000-0000-0000-00000000000b",
"name": "Place 75"
},
"package": {
"batch_number": "SRC-1",
"compliance_label": "ABCDEF012345670000000006",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-000000000004",
"license_id": "00000000-0000-0000-0000-00000000000b",
"location_id": "00000000-0000-0000-0000-00000000004c",
"metrc_id": 6,
"metrc_label": "ABCDEF012345670000000006",
"quantity": "5.000000000",
"quantity_active": "5.000000000",
"status": "active"
},
"product": {
"id": "07fce643-63bf-498c-a2c1-7eee6f192210",
"name": "Product 159",
"sku": "sku 160",
"updated_datetime": "2026-08-24T16:01:04.414458Z"
},
"quantity": "85.048477632",
"status": "COMPLETED",
"total_cost_actual": null,
"total_cost_default": null
}
],
"is_donation": false,
"is_finished_good": false,
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000171",
"id": "00000000-0000-0000-0000-00000000004c",
"license_id": "00000000-0000-0000-0000-00000000000b",
"name": "Place 75"
},
"metrc_item_id": null,
"metrc_location_id": null,
"metrc_notes": "eighths",
"metrc_production_batch_number": null,
"package": {
"batch_number": "OUT-1",
"compliance_label": "1A4010200001234000000001",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-000000000006",
"license_id": "00000000-0000-0000-0000-00000000000b",
"location_id": "00000000-0000-0000-0000-00000000004c",
"metrc_id": null,
"metrc_label": "1A4010200001234000000001",
"quantity": "3.000000000",
"quantity_active": "3.000000000",
"status": "active"
},
"package_date": "2026-08-24",
"package_datetime": "2026-08-24",
"package_unit_type": {
"id": "00000000-0000-0000-0000-0000000011c3",
"name": "Ounce"
},
"product": {
"id": "07fce643-63bf-498c-a2c1-7eee6f192210",
"name": "Product 159",
"sku": "sku 160",
"updated_datetime": "2026-08-24T16:01:04.414458Z"
},
"quantity": "85.048477632",
"status": "COMPLETED",
"total_cost_actual": null,
"total_cost_default": null,
"use_same_item": true
},
{
"additional_costs": [],
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000002f",
"name": "B161"
},
"batch_number": null,
"bins": [],
"compliance_label": "1A4010200001234000000002",
"compliance_quantity": "2",
"copy_custom_data_from_input": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"costs": [],
"expiration_date": null,
"expiration_datetime": null,
"id": "b03682ef-1907-4474-bba6-7cb713929a60",
"ingredients": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000030",
"name": "B162"
},
"compliance_quantity": "2",
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "b9c6861b-d4df-4c4d-9110-2ac3bef2cc22",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000171",
"id": "00000000-0000-0000-0000-00000000004c",
"license_id": "00000000-0000-0000-0000-00000000000b",
"name": "Place 75"
},
"package": {
"batch_number": "SRC-1",
"compliance_label": "ABCDEF012345670000000006",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-000000000004",
"license_id": "00000000-0000-0000-0000-00000000000b",
"location_id": "00000000-0000-0000-0000-00000000004c",
"metrc_id": 6,
"metrc_label": "ABCDEF012345670000000006",
"quantity": "5.000000000",
"quantity_active": "5.000000000",
"status": "active"
},
"product": {
"id": "07fce643-63bf-498c-a2c1-7eee6f192210",
"name": "Product 159",
"sku": "sku 160",
"updated_datetime": "2026-08-24T16:01:04.414458Z"
},
"quantity": "56.698985088",
"status": "COMPLETED",
"total_cost_actual": null,
"total_cost_default": null
}
],
"inputs": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000030",
"name": "B162"
},
"compliance_quantity": "2",
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "b9c6861b-d4df-4c4d-9110-2ac3bef2cc22",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000171",
"id": "00000000-0000-0000-0000-00000000004c",
"license_id": "00000000-0000-0000-0000-00000000000b",
"name": "Place 75"
},
"package": {
"batch_number": "SRC-1",
"compliance_label": "ABCDEF012345670000000006",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-000000000004",
"license_id": "00000000-0000-0000-0000-00000000000b",
"location_id": "00000000-0000-0000-0000-00000000004c",
"metrc_id": 6,
"metrc_label": "ABCDEF012345670000000006",
"quantity": "5.000000000",
"quantity_active": "5.000000000",
"status": "active"
},
"product": {
"id": "07fce643-63bf-498c-a2c1-7eee6f192210",
"name": "Product 159",
"sku": "sku 160",
"updated_datetime": "2026-08-24T16:01:04.414458Z"
},
"quantity": "56.698985088",
"status": "COMPLETED",
"total_cost_actual": null,
"total_cost_default": null
}
],
"is_donation": false,
"is_finished_good": false,
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000171",
"id": "00000000-0000-0000-0000-00000000004c",
"license_id": "00000000-0000-0000-0000-00000000000b",
"name": "Place 75"
},
"metrc_item_id": 1000001,
"metrc_location_id": null,
"metrc_notes": null,
"metrc_production_batch_number": null,
"package": {
"batch_number": null,
"compliance_label": "1A4010200001234000000002",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-000000000007",
"license_id": "00000000-0000-0000-0000-00000000000b",
"location_id": "00000000-0000-0000-0000-00000000004c",
"metrc_id": null,
"metrc_label": "1A4010200001234000000002",
"quantity": "2.000000000",
"quantity_active": "2.000000000",
"status": "active"
},
"package_date": "2026-08-24",
"package_datetime": "2026-08-24",
"package_unit_type": {
"id": "00000000-0000-0000-0000-0000000011c3",
"name": "Ounce"
},
"product": {
"id": "07fce643-63bf-498c-a2c1-7eee6f192210",
"name": "Product 159",
"sku": "sku 160",
"updated_datetime": "2026-08-24T16:01:04.414458Z"
},
"quantity": "56.698985088",
"status": "COMPLETED",
"total_cost_actual": null,
"total_cost_default": null,
"use_same_item": false
}
],
"owner_id": "00000000-0000-0000-0000-0000000001ba",
"status": "COMPLETED",
"updated_datetime": "2026-08-24T16:01:04.779933Z",
"waste_count_quantity": null,
"waste_count_unit_name": null,
"waste_volume_quantity": null,
"waste_volume_unit_name": null,
"waste_weight_quantity": null,
"waste_weight_unit_name": null
}
}
Error scenario: missing source package
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 844e0fc9605eb0b1336d371e2fac097a-02f73e57afbe462c-0
{
"errors": [
{
"context": {},
"message": "can't be blank",
"pointer": [
"source_package_id"
],
"section": "body"
}
]
}
Error scenario: insufficient source quantity
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 8a39f981f88bbce50183b7d364473c2d-01fb39595fa16a25-0
{
"errors": [
{
"context": {},
"message": "Insufficient quantity. Needed to move 9999 but only found 10 available.",
"pointer": [
"outputs",
0,
"input_compliance_quantity"
]
}
]
}
Error scenario: cost type not found
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 0b5945aa804ba4156a1202fe8e01fc91-e2802ed2da698c8c-0
{
"errors": [
{
"context": {},
"message": "cost_type could not be found",
"pointer": [
"outputs",
0,
"costs",
0,
"cost_type_id"
]
}
]
}
Split a single Metrc source package into multiple output packages.
Creates one assembly containing every input and output. On success each output package is queued for creation in Metrc and synced asynchronously — the Metrc package identifiers are not present in the immediate response, and the source and output packages are briefly flagged as syncing.
Metrc licenses only. Up to 300 outputs per request. The whole split is applied atomically: if any output is rejected, none are created.
Required permission: assemblies_permissions_create.
Request
POST /public/v1/assemblies/split_package
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| outputs | The output packages to create, between 1 and 300 | body | array(SplitPackageOutput) | true | ||
| source_package_id | The package to split. Must be package-tracked and in a Metrc license. | body | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 201 | The created assembly | AssemblyResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert an assembly
Success scenario
POST /public/v1/assemblies
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzAsImlhdCI6MTc4NzU4NzI3MCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYzVjN2UyMTAtMTcwNi00MGJlLTk3NzctNDdlOThjZmUxY2IyIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY5LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjI0OCIsInR5cCI6ImFjY2VzcyJ9.dde6A4iTuvp8D7uGzY7t5N0ayh4VTUY5cQBcuUKy8as
{
"action": "CREATE",
"outputs": [
{
"action": "CREATE",
"compliance_label": "1A4010200001234000000030",
"compliance_quantity": 5,
"inputs": [
{
"action": "CREATE",
"compliance_quantity": 5,
"package_id": "00000000-0000-0000-0000-00000000003e",
"status": "PENDING"
}
],
"location_id": "00000000-0000-0000-0000-0000000001b7",
"metrc_notes": "some notes",
"metrc_production_batch_number": "PB-1",
"product_id": "7c219e95-4cff-42ba-9b6a-791132f80120",
"status": "PENDING",
"use_same_item": true
}
],
"status": "PENDING"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c02a69cd4a34cdbfcd76f43b9f1276d6-5ee9be1a4059e570-0
{
"data": {
"assembly_number": "AS-0000001",
"completion_datetime": null,
"compliance_type": "METRC",
"creation_source": "MANUALLY_CREATED",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-2237@example.com",
"full_name": "FirstName4560 LastName4561",
"id": "00000000-0000-0000-0000-0000000008c8",
"inserted_datetime": "2026-08-24T16:01:10.264309Z",
"role": {
"id": "00000000-0000-0000-0000-0000000008f4",
"name": "Admin 2291"
}
},
"custom_data": [],
"description": null,
"estimated_start_date": null,
"estimated_start_datetime": null,
"estimated_work_hours": null,
"estimated_work_minutes": null,
"fulfilled": true,
"id": "082676f4-6fe9-4c9d-b25c-18b561bc72f7",
"inserted_datetime": "2026-08-24T16:01:10.627981Z",
"is_metrc_processing_job": false,
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:10.281802Z",
"id": "00000000-0000-0000-0000-000000000059",
"inserted_datetime": "2026-08-24T16:01:10.281878Z",
"issue_datetime": "2026-08-24T16:01:10.281801Z",
"license_number": "CDPH-00000092",
"license_type": "Other"
},
"metrc_processing_job": null,
"metrc_processing_job_id": null,
"metrc_processing_job_name": null,
"metrc_processing_job_notes": null,
"metrc_processing_job_type_id": null,
"outputs": [
{
"additional_costs": [],
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000151",
"name": "B1017"
},
"batch_number": null,
"bins": [],
"compliance_label": "1A4010200001234000000030",
"compliance_quantity": "5",
"copy_custom_data_from_input": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"costs": [],
"expiration_date": null,
"expiration_datetime": null,
"id": "52aa7554-f8e7-4118-a122-731193406ab0",
"ingredients": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000151",
"name": "B1017"
},
"compliance_quantity": "5",
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "8b9a0c83-8539-4bcd-829f-c5b63a01c17d",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000638",
"id": "00000000-0000-0000-0000-0000000001b7",
"license_id": "00000000-0000-0000-0000-000000000059",
"name": "Place 438"
},
"package": {
"batch_number": null,
"compliance_label": "ABCDEF012345670000000115",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-00000000003e",
"license_id": "00000000-0000-0000-0000-000000000059",
"location_id": "00000000-0000-0000-0000-0000000001b7",
"metrc_id": 115,
"metrc_label": "ABCDEF012345670000000115",
"quantity": "10.000000000",
"quantity_active": "5.000000000",
"status": "active"
},
"product": {
"id": "7c219e95-4cff-42ba-9b6a-791132f80120",
"name": "Product 1013",
"sku": "sku 1014",
"updated_datetime": "2026-08-24T16:01:10.327032Z"
},
"quantity": "141.74746272",
"status": "PENDING",
"total_cost_actual": null,
"total_cost_default": null
}
],
"inputs": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000151",
"name": "B1017"
},
"compliance_quantity": "5",
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "8b9a0c83-8539-4bcd-829f-c5b63a01c17d",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000638",
"id": "00000000-0000-0000-0000-0000000001b7",
"license_id": "00000000-0000-0000-0000-000000000059",
"name": "Place 438"
},
"package": {
"batch_number": null,
"compliance_label": "ABCDEF012345670000000115",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-00000000003e",
"license_id": "00000000-0000-0000-0000-000000000059",
"location_id": "00000000-0000-0000-0000-0000000001b7",
"metrc_id": 115,
"metrc_label": "ABCDEF012345670000000115",
"quantity": "10.000000000",
"quantity_active": "5.000000000",
"status": "active"
},
"product": {
"id": "7c219e95-4cff-42ba-9b6a-791132f80120",
"name": "Product 1013",
"sku": "sku 1014",
"updated_datetime": "2026-08-24T16:01:10.327032Z"
},
"quantity": "141.74746272",
"status": "PENDING",
"total_cost_actual": null,
"total_cost_default": null
}
],
"is_donation": false,
"is_finished_good": false,
"is_production_batch": true,
"is_test_sample": false,
"is_trade_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000638",
"id": "00000000-0000-0000-0000-0000000001b7",
"license_id": "00000000-0000-0000-0000-000000000059",
"name": "Place 438"
},
"metrc_item_id": null,
"metrc_location_id": null,
"metrc_notes": "some notes",
"metrc_production_batch_number": "PB-1",
"package": null,
"package_date": "2026-08-24",
"package_datetime": "2026-08-24",
"package_unit_type": {
"id": "00000000-0000-0000-0000-000000004d8e",
"name": "Ounce"
},
"product": {
"id": "7c219e95-4cff-42ba-9b6a-791132f80120",
"name": "Product 1013",
"sku": "sku 1014",
"updated_datetime": "2026-08-24T16:01:10.327032Z"
},
"quantity": "141.74746272",
"status": "PENDING",
"total_cost_actual": null,
"total_cost_default": null,
"use_same_item": true
}
],
"owner_id": "00000000-0000-0000-0000-0000000008c8",
"status": "PENDING",
"updated_datetime": "2026-08-24T16:01:10.627981Z",
"waste_count_quantity": null,
"waste_count_unit_name": null,
"waste_volume_quantity": null,
"waste_volume_unit_name": null,
"waste_weight_quantity": null,
"waste_weight_unit_name": null
}
}
Error scenario: missing action
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 1227cb77642b8353406eceb8f97b4f3c-0423c50e0d9aed5c-0
{
"errors": [
{
"context": {},
"message": "can't be blank",
"pointer": [
"action"
],
"section": "body"
}
]
}
Error scenario: output quantity must be positive
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 5eed42e6e881f9c6ddf959658de5fb5d-3fd0ac54607a99b4-0
{
"errors": [
{
"context": {
"id": "17f3002a-2586-461d-a2dc-7e789cb4183a"
},
"message": "must be greater than 0",
"pointer": [
"outputs",
0,
"quantity"
],
"section": "body"
}
]
}
Error scenario: input batch not found
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b9037cd1bfb06e4ab22c67ee4ec8a699-c8d3e1e9cda8e075-0
{
"errors": [
{
"context": {},
"message": "batch could not be found",
"pointer": [
"outputs",
1,
"inputs",
0,
"batch_id"
]
}
]
}
Error scenario: cost type not found
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 2039f104d1d319addebddb7db614f956-450d9e4d17c2e285-0
{
"errors": [
{
"context": {},
"message": "cost_type could not be found",
"pointer": [
"outputs",
0,
"costs",
0,
"cost_type_id"
]
}
]
}
Create, update, or delete an assembly and its outputs, inputs, and costs in a single request.
Every row — the assembly and each nested output, input, and cost — carries a required action of
CREATE, UPDATE, or DELETE. UPDATE and DELETE must include the row's id; CREATE omits
it. DELETE on the assembly removes it along with all of its outputs, inputs, and costs. Updates
are sparse: only the fields you send are changed. A nested row (output, input, or cost) you leave
out entirely is left untouched — omission never deletes it; removing one always requires sending it
with action DELETE. The whole request is applied atomically: if any row is rejected, none of the
changes are saved.
How this moves inventory: while the assembly is PENDING, each fulfilled input (status PENDING
or COMPLETED) claims specific on-hand inventory from its batch or package, and each DRAFT input
reserves product-level quantity without committing a specific lot. Completing an output consumes its
inputs and produces the output into inventory at its location_id — as a batch for batch-/product-
tracked outputs, or as a new package for package-tracked (Metrc) outputs. Completing the whole
assembly requires all of its outputs to be completed.
How this moves compliance: on a Metrc license, completing a package-tracked output (or a Metrc
processing job) is pushed to Metrc as a side effect after the request commits. A 2xx therefore means
the change was saved in Distru, not that Metrc has finished syncing — the created packages' Metrc
identifiers may still be absent and the affected packages briefly flagged as syncing. Re-fetch the
assembly with GET /public/v1/assemblies/{id} to observe the synced result. This endpoint supports
Metrc and non-compliance (NONE) licenses only; BioTrack is not supported.
Only assemblies with creation_source=MANUALLY_CREATED (i.e. created via the Assembly form in
Distru or via the API) can be modified or deleted by this endpoint; system-generated assemblies
(SALES_ORDER, SPLIT_PACKAGE, LAB_TESTING) are rejected.
Metrc processing jobs: set metrc_processing_job.name and metrc_processing_job.type_id together
to make the assembly a Metrc processing job. Both are required together, the license must be a
Metrc license with processing-job capability, type_id must be the Metrc ID of an existing Metrc
processing job type, and name must be non-empty and not already used by a processing job in Metrc.
Once set, name and type_id are permanent — they
cannot be changed on a later update; only notes and waste stay editable. metrc_processing_job.id
in the response is the job's Metrc-assigned ID (set by Metrc once Distru creates the job there); it
is read-only and null until then. Completing a processing job (setting status to COMPLETED) requires
notes. waste is what Distru reports to Metrc when the job is finished (each quantity sent with
its unit name); record it while the assembly is still PENDING — at the latest in the same request
that completes it. Once the assembly is COMPLETED the waste fields are read-only.
Completed assemblies: once an assembly's status is COMPLETED, the only assembly-level fields
you can still change are description, custom_data, estimated_work_hours,
estimated_work_minutes, and owner_id; every other assembly field is read-only, and the assembly
can be neither un-completed nor deleted. Its outputs and inputs are frozen — they cannot be edited
or deleted. Costs behave differently: an existing cost on a completed output cannot be edited or
deleted, but you can still add new costs to that output. Completing an assembly requires all of its
outputs to be completed. For a Metrc processing job, packages (inputs) cannot be added or removed
once any output is completed; to delete such an assembly, finish the job in Metrc and Distru removes
it automatically about 30 minutes later.
Required permission: assemblies_permissions_create to create, assemblies_permissions_edit
to update, assemblies_permissions_delete to delete.
Request
POST /public/v1/assemblies
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| action | CREATE, UPDATE, or DELETE. Required. DELETE removes the assembly and all of its outputs, inputs, and costs. CREATE UPDATE DELETE |
body | string | true | ||
| custom_data | 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. | body | object | false | ||
| description | A free-text description for this assembly. Editable at any status. | body | string | false | ||
| estimated_start_datetime | 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. |
body | string | false | ||
| estimated_work_hours | 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. |
body | integer | false | ||
| estimated_work_minutes | The minutes portion of the estimated work time; must be 0 or greater. Pairs with estimated_work_hours (see above). Editable at any status. |
body | integer | false | ||
| id | The assembly to update or delete. Required for UPDATE and DELETE; omit for CREATE. | body | string | false | ||
| metrc_processing_job | The Metrc processing job details for an assembly | body | UpsertAssemblyMetrcProcessingJob | false | ||
| outputs | 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. |
body | array(UpsertAssemblyOutput) | false | ||
| owner_id | The ID of the user that owns this assembly. Optional. Editable at any status. | body | string | false | ||
| status | 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).PENDING COMPLETED |
body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated assembly | AssemblyResponse |
| 201 | The created assembly | AssemblyResponse |
| 204 | The assembly was deleted | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Batch
Create or update a batch
Success scenario
POST /public/v1/batches
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzEsImlhdCI6MTc4NzU4NzI3MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYjE4MzliZmUtMzdkZS00ZDE5LWI1NzQtNGRjZDJkY2ViMjg4IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjYwNCIsInR5cCI6ImFjY2VzcyJ9.VB_CYOeaaGLkK3N7CyeV84mUlfZIvruwwfVDMn8IsiQ
{
"batch_number": "B1",
"cbd": "0.3%",
"custom_data": {
"87": [
"A",
"B"
]
},
"description": "Test batch",
"expiration_datetime": "2025-01-01T00:00:00.000000Z",
"harvest_datetime": "2024-06-15T00:00:00.000000Z",
"manufactured_datetime": "2025-01-02T03:04:05.000000Z",
"name": "Custom Batch Name",
"owner_id": "00000000-0000-0000-0000-000000000a36",
"product_id": "016ddd8e-1a67-4bdf-9b0c-4cc7f4d5be59",
"thc": "18.5%"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 3300a0b4adca07f35d51f99ea02e439d-982b933f505378ef-0
{
"data": {
"batch_number": "B1",
"cbd": "0.3%",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-2592@example.com",
"full_name": "FirstName5280 LastName5281",
"id": "00000000-0000-0000-0000-000000000a2c",
"inserted_datetime": "2026-08-24T16:01:11.535832Z",
"role": {
"id": "00000000-0000-0000-0000-000000000a61",
"name": "Admin 2656"
}
},
"custom_data": [
{
"id": 87,
"name": "Custom Field 61",
"value": "A,B"
}
],
"deleted_at": null,
"description": "Test batch",
"expiration_date": "2025-01-01T00:00:00.000000Z",
"expiration_datetime": "2025-01-01T00:00:00.000000Z",
"harvest_datetime": "2024-06-15T00:00:00.000000Z",
"id": "00000000-0000-0000-0000-0000000001a6",
"inserted_datetime": "2026-08-24T16:01:11.585669Z",
"manufactured_datetime": "2025-01-02T03:04:05.000000Z",
"name": "Custom Batch Name",
"owner_id": "00000000-0000-0000-0000-000000000a36",
"product": {
"id": "016ddd8e-1a67-4bdf-9b0c-4cc7f4d5be59",
"name": "Product 1286",
"sku": "sku 1287",
"updated_datetime": "2026-08-24T16:01:11.560646Z"
},
"product_id": "016ddd8e-1a67-4bdf-9b0c-4cc7f4d5be59",
"quantity_active": "0",
"quantity_active_by_location": [],
"thc": "18.5%",
"updated_datetime": "2026-08-24T16:01:11.585669Z"
}
}
Error scenario: invalid product id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 9eee52f8ce555a0eeffcaa5269ab3338-c7435661fd98863a-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"product_id"
],
"section": "body"
}
]
}
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.
A 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.
On 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, 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.
Update 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).
Required permission: products_permissions_create to create, products_permissions_edit to update.
Request
POST /public/v1/batches
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_number | 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. |
body | string | false | ||
| bin_ids | 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. |
body | array | false | ||
| cbd | 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. | body | string | false | ||
| custom_data | 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. | body | object | false | {"101":"Some text value","102":"2026-08-18T00:00:00.000-07:00","103":["Option A","Option B"]} | |
| description | Free-form notes about the batch. Nullable and optional. | body | string | false | ||
| expiration_datetime | 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. |
body | string | false | ||
| harvest_datetime | 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. |
body | string | false | ||
| id | 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. | body | string | false | ||
| manufactured_datetime | 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. |
body | string | false | ||
| name | 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. |
body | string | false | ||
| owner_id | 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. | body | string | false | ||
| product_id | 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. | body | string | false | ||
| thc | 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. | body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single batch | BatchFullResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Delete a batch
Success scenario
DELETE /public/v1/batches/dbf34ffb-5e7e-4b7d-9b41-cf381f27b4bc
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzMsImlhdCI6MTc4NzU4NzI3MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmE0NTNlYjgtNjRkZS00NTNlLWI0NGEtOGE3ZWYxYTBjYzk1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzE0MiIsInR5cCI6ImFjY2VzcyJ9.prQ9oJP7X1X_OLC9F0AlSUp3TwgJHRCOSQn0mm-JP5o
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 8c2f5a1d94e3ba702811d6c7d10286db-4b7edcd33594b7aa-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cf914631d8868f824545ba38e52cf1ab-aa618aa77fb9cfbe-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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).
A 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.
A 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 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.
Required permission: products_permissions_delete.
Request
DELETE /public/v1/batches/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a batch
Success scenario
GET /public/v1/batches/00000000-0000-0000-0000-00000000011b
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjksImlhdCI6MTc4NzU4NzI2OSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZjJlNDQ0NDAtYzg2Ni00N2NmLTkyYjktMWIzZjZiMDU4YWM3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY4LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTgyNSIsInR5cCI6ImFjY2VzcyJ9.ASEIdLbOZ2IVKCGLcmbdbBH-vjpP2pgX9FuLwJ8WJn8
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: dfc2ccaa408d40483e3026b934d442bb-e9386369cb622ed8-0
{
"data": {
"batch_number": "B001",
"cbd": null,
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1834@example.com",
"full_name": "FirstName3732 LastName3733",
"id": "00000000-0000-0000-0000-000000000730",
"inserted_datetime": "2026-08-24T16:01:09.061355Z",
"role": {
"id": "00000000-0000-0000-0000-000000000754",
"name": "Admin 1875"
}
},
"custom_data": [
{
"id": 63,
"name": "Custom Field 41",
"value": "Custom Data 1"
}
],
"deleted_at": null,
"description": "Test batch",
"expiration_date": null,
"expiration_datetime": null,
"harvest_datetime": null,
"id": "00000000-0000-0000-0000-00000000011b",
"inserted_datetime": "2026-08-24T16:01:09.081989Z",
"manufactured_datetime": "2026-08-24T16:01:08.954031Z",
"name": "B844",
"owner_id": "00000000-0000-0000-0000-000000000737",
"primary_test_result": null,
"product": {
"id": "39d57a67-a2bb-4031-b1ee-16c04176268c",
"name": "Product 840",
"sku": "sku 841",
"updated_datetime": "2026-08-24T16:01:09.051721Z"
},
"product_id": "39d57a67-a2bb-4031-b1ee-16c04176268c",
"quantity_active": "0",
"quantity_active_by_location": [],
"thc": null,
"updated_datetime": "2026-08-24T16:01:09.081989Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f5441600ed0939b4d928b72aa3d6042e-3c34a23ff1e329ec-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Required permission: products_permissions_view.
Request
GET /public/v1/batches/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The Distru batch ID to fetch. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single batch | BatchFullResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get batches
Success scenario
GET /public/v1/batches
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiZDg0NGIyYzgtNWJiYi00ZTczLTk3YjgtZjVhYTBkZWUwYjU4IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjA2IiwidHlwIjoiYWNjZXNzIn0.0MrI97GVezO-3Lgd1Al1q83kOnhCM4c97ZHBJ_tBBrk
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ad299dcf70eff6f7583ac388f9f338bc-357c446c942d7863-0
{
"data": [
{
"batch_number": null,
"cbd": null,
"creator": null,
"custom_data": [
{
"id": 3,
"name": "Custom Field 2",
"value": "Custom Data 1"
}
],
"deleted_at": null,
"description": null,
"expiration_date": "2024-01-01T00:00:00.000000Z",
"expiration_datetime": "2024-01-01T00:00:00.000000Z",
"harvest_datetime": null,
"id": "00000000-0000-0000-0000-00000000001c",
"inserted_datetime": "2026-08-24T16:01:03.411569Z",
"manufactured_datetime": "2024-01-02T03:04:05.000000Z",
"name": "B81",
"owner_id": "00000000-0000-0000-0000-0000000000d5",
"primary_test_result": null,
"product": {
"id": "d4ec2cae-66a2-4a04-9cfb-5f240d0785be",
"name": "Product 79",
"sku": "sku 80",
"updated_datetime": "2026-08-24T16:01:03.408998Z"
},
"product_id": "d4ec2cae-66a2-4a04-9cfb-5f240d0785be",
"quantity_active": "0",
"quantity_active_by_location": [],
"thc": null,
"updated_datetime": "2026-08-24T16:01:03.411569Z"
},
{
"batch_number": null,
"cbd": "0.5",
"creator": null,
"custom_data": [
{
"id": 3,
"name": "Custom Field 2",
"value": null
}
],
"deleted_at": null,
"description": null,
"expiration_date": null,
"expiration_datetime": null,
"harvest_datetime": "2024-06-15T00:00:00.000000Z",
"id": "00000000-0000-0000-0000-00000000001d",
"inserted_datetime": "2026-08-24T16:01:03.433791Z",
"manufactured_datetime": "2024-01-02T03:04:05.000000Z",
"name": "B84",
"owner_id": "00000000-0000-0000-0000-0000000000d8",
"primary_test_result": {
"cbd_mg_per_unit": "1",
"cbd_mg_per_unit_total": "2",
"cbd_percentage": "3",
"cbd_percentage_total": "4",
"coa_url": null,
"mg_per_unit_type": "mg/mL",
"name": "File.pdf",
"thc_mg_per_unit": "5",
"thc_mg_per_unit_total": "6",
"thc_percentage": "7",
"thc_percentage_total": "8"
},
"product": {
"id": "b65dd7d8-882a-4cde-87da-99b8ee0ac1b7",
"name": "Product 82",
"sku": "sku 83",
"updated_datetime": "2026-08-24T16:01:03.428328Z"
},
"product_id": "b65dd7d8-882a-4cde-87da-99b8ee0ac1b7",
"quantity_active": "0",
"quantity_active_by_location": [],
"thc": "22.5",
"updated_datetime": "2026-08-24T16:01:03.433791Z"
}
],
"next_page": null
}
Error scenario: malformed date range filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: d9512fa47e2db28e7e8bc7bcbc03d39d-3ca383b70018a2aa-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"updated_datetime"
],
"section": "query"
}
]
}
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.
Results 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.
By 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.
This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.
Required 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.
Request
GET /public/v1/batches
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_number | 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. |
query | string | false | ?batch_number=LOT-124 | |
| batch_numbers | 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. |
query | array | false | ||
| custom_data | 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. |
query | object | false | ?custom_data[101]=Blue&custom_data[102]=Wholesale | |
| deleted | 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.no include only |
query | string | false | no | |
| has_quantity_active | Keep only batches that currently hold active quantity (true) or none (false). Omit to match either. |
query | boolean | false | ||
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| include_costs | 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}. |
query | boolean | false | false | ?include_costs=true |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z, | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| product_brand_ids | Return only batches whose product has any of these brands (matches the batch's product.brand.id). Multiple ids are OR-ed. |
query | array | false | ||
| product_category_ids | Return only batches whose product is in any of these categories (matches the batch's product.category.id). Multiple ids are OR-ed. |
query | array | false | ||
| product_group_ids | Return only batches whose product is in any of these groups (matches the batch's product.product_group.id). Multiple ids are OR-ed. |
query | array | false | ||
| product_ids | 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. |
query | array | false | ?product_ids[]=00000000-0000-0000-0000-000000000001&product_ids[]=00000000-0000-0000-0000-000000000002 | |
| product_skus | 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. |
query | array | false | ||
| product_strain_ids | Return only batches whose product has any of these strains (matches the batch's product.strain.id). Multiple ids are OR-ed. |
query | array | false | ||
| product_subcategory_ids | Return only batches whose product is in any of these subcategories (matches the batch's product.subcategory.id). Multiple ids are OR-ed. |
query | array | false | ||
| product_tag_ids | 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. |
query | array | false | ||
| product_vendor_ids | 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. |
query | array | false | ||
| updated_datetime | 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. |
query | string | false | ?updated_datetime=,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of batches | Batches |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Bin
Delete a bin
Success scenario
DELETE /public/v1/bins/1b751ce0-c1ad-467f-92e1-bb681f310de6
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiNTM3MTI2OTItOWViZS00MzFkLWJkMGYtN2YyOTk0MTZkNTY5IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTM0NyIsInR5cCI6ImFjY2VzcyJ9.bD6--kGxSq1kGn7CsjoY9ZuQttevW80O9mJ2Sx-FVvg
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 56a894578208b4f76b8c27885e7fbdde-f02ad4a6ef5c1597-0
Error scenario: bin inventory tracking disabled
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 10ec186a6bdcb19be9eed54f79338d75-92ef10122def157b-0
{
"errors": [
{
"context": {},
"message": "Bin inventory tracking is not enabled for your company",
"pointer": [
"base"
]
}
]
}
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.
Deleting 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.
Returns 404 if no bin with that ID belongs to your company (the lookup is company-scoped), and 204 with no body on success.
Bins only exist for companies that have bin inventory tracking enabled; if it is disabled the request is rejected.
Required permission: settings_permissions_bins.
Request
DELETE /public/v1/bins/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The bin's ID. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a bin
Success scenario
GET /public/v1/bins/ec41bdb1-6034-4005-8dac-688a2bdce970
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYTRhYmQ3ZDAtM2UyYS00Y2EwLWEyMmItNTExNDJjMTQ4MGI1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTAwMiIsInR5cCI6ImFjY2VzcyJ9.q0ehm7QXIo_jWLivSuikCWUem3g7PMmQ73HyY8IN52Q
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 544cce80c03c48495b2ef1b16fcef749-ef9a361761cd3834-0
{
"data": {
"id": "ec41bdb1-6034-4005-8dac-688a2bdce970",
"inserted_datetime": "2026-08-24T16:01:06.426139Z",
"name": "Cold Room",
"updated_datetime": "2026-08-24T16:01:06.426139Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c232d786924d091b50ad12bbab2f151b-c4b677a494dc420e-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Fetch a single bin by ID.
Returns 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.
Required permission: settings_permissions_bins.
Request
GET /public/v1/bins/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The bin's ID. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single bin | BinResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get bins
Success scenario
GET /public/v1/bins
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMjg1M2M0NGItNmExOC00NDk1LTljZmQtNGU1NzBhYWNjZDc4IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6Njk2IiwidHlwIjoiYWNjZXNzIn0.u2_kcATA13vhdZJAiwR1kbNYXhM7nqXPEFvhdFI6PXI
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b6866b97014a3a66c4c3a6cb38664ae4-66f6f7e29b60a824-0
{
"data": [
{
"id": "4ca87cd5-4921-4ea8-b34c-897143805c8f",
"inserted_datetime": "2026-08-24T16:01:05.304860Z",
"name": "AAA",
"updated_datetime": "2026-08-24T16:01:05.304860Z"
},
{
"id": "7e356b0a-cf94-4830-849b-a70462dfea35",
"inserted_datetime": "2026-08-24T16:01:05.306731Z",
"name": "BBB",
"updated_datetime": "2026-08-24T16:01:05.306731Z"
},
{
"id": "9473e396-2ab5-45c3-bea9-9f4cb67684ad",
"inserted_datetime": "2026-08-24T16:01:05.308164Z",
"name": "CCC",
"updated_datetime": "2026-08-24T16:01:05.308164Z"
}
],
"next_page": "https://www.example.com/public/v1/bins?page[number]=2"
}
Error scenario: malformed page param
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 71d287f7215e738884dca5fb24539c7d-d788ebc8594533f4-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
List the bins belonging to the authenticated company, sorted alphabetically by name (A→Z, case-insensitive).
A 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.
Results are paginated; follow next_page in the response envelope to page forward, or stop when it is null.
Required permission: settings_permissions_bins.
Request
GET /public/v1/bins
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| search | 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". |
query | string | false | ||
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of bins | Bins |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a bin
Success scenario
POST /public/v1/bins
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiZDk4NWRiOGItYzc3YS00MmViLTljODAtYWY5ZDI3N2I4OTkwIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTE4MCIsInR5cCI6ImFjY2VzcyJ9.jyABvBnA6bRmCF0eSzH01c2cAe9mBvyO-PoX_wtpjfQ
{
"name": "Vault"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 04f049770661d26c798256025b25b951-d983b84ef3b2bed7-0
{
"data": {
"id": "763ac3d7-4da7-4c75-9acb-288849914322",
"inserted_datetime": "2026-08-24T16:01:07.037140Z",
"name": "Vault",
"updated_datetime": "2026-08-24T16:01:07.037140Z"
}
}
Error scenario: bin inventory tracking disabled
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 83112741fe5320a77a69ef51729c9237-16496c6158632649-0
{
"errors": [
{
"context": {},
"message": "Bin inventory tracking is not enabled for your company",
"pointer": [
"base"
]
}
]
}
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.
A 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.
Names 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.
Bins 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.
Required permission: settings_permissions_bins.
Request
POST /public/v1/bins
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. |
body | string | false | ||
| name | 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. | body | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated bin | BinResponse |
| 201 | The created bin | BinResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Company
Delete a company
Success scenario
DELETE /public/v1/companies/00000000-0000-0000-0000-000000000392
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzIsImlhdCI6MTc4NzU4NzI3MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTgwYzI0OTUtZDJkMy00YjYyLTkwYmQtNzFjZTgxZjNmNTY3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjcxMyIsInR5cCI6ImFjY2VzcyJ9.s7KFtB4rRjgRKgw9M3T4eyVumzxJCHaqOswiJRsyJYQ
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 5448ba9743f5da802811d6c7d10286db-5a5edcd33594b7bb-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ba814631d8868f824545ba38e52cf1fa-ff618aa77fb9cfae-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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).
The 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 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.
Required permission: companies_permissions_delete.
Request
DELETE /public/v1/companies/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a company
Success scenario
GET /public/v1/companies/00000000-0000-0000-0000-000000000392
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzIsImlhdCI6MTc4NzU4NzI3MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTgwYzI0OTUtZDJkMy00YjYyLTkwYmQtNzFjZTgxZjNmNTY3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjcxMyIsInR5cCI6ImFjY2VzcyJ9.s7KFtB4rRjgRKgw9M3T4eyVumzxJCHaqOswiJRsyJYQ
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f7d02c5011c5a937528cefcceb59b773-fe7ef3e0d793a1a8-0
{
"data": {
"category": "Retailer",
"custom_data": [
{
"id": 89,
"name": "Custom Field 63",
"value": "Custom Value"
}
],
"default_email": "co@example.com",
"default_payment_term": {
"days": 15,
"id": "00000000-0000-0000-0000-000000000009",
"inserted_datetime": "2026-08-24T16:01:12.040998Z",
"locked": false,
"name": "Net 15",
"time_of_day": "17:00:00",
"updated_datetime": "2026-08-24T16:01:12.040998Z"
},
"default_purchase_order_notes": null,
"default_sales_order_notes": null,
"deleted_at": null,
"group": {
"id": "00000000-0000-0000-0000-000000000016",
"name": "Comp Rel Group 20"
},
"id": "00000000-0000-0000-0000-000000000392",
"inserted_datetime": "2026-08-24T16:01:12.049718Z",
"invoice_email": "inv@example.com",
"leaflink_brand_id": null,
"leaflink_customer_id": null,
"legal_business_name": "Legal Co",
"licenses": [
{
"active": true,
"expiry_datetime": "2026-09-24T16:01:12.038916Z",
"id": "00000000-0000-0000-0000-000000000074",
"inserted_datetime": "2026-08-24T16:01:12.039007Z",
"issue_datetime": "2026-08-24T16:01:12.038915Z",
"license_number": "CDPH-00000119",
"license_type": "Specialty Cottage Indoor"
}
],
"locations": [
{
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000783",
"id": "00000000-0000-0000-0000-000000000205",
"license_id": null,
"name": "Place 516"
}
],
"name": "Company 1916",
"order_shipment_email": null,
"outstanding_balance": "0",
"outstanding_balance_threshold": null,
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-2713@example.com",
"full_name": "FirstName5522 LastName5523",
"id": "00000000-0000-0000-0000-000000000aa6",
"inserted_datetime": "2026-08-24T16:01:12.046788Z",
"role": {
"id": "00000000-0000-0000-0000-000000000ae1",
"name": "Admin 2784"
}
},
"owner_id": "00000000-0000-0000-0000-000000000aa6",
"phone_number": null,
"purchase_order_email": null,
"qb_customer_id": null,
"qb_vendor_id": null,
"relationship_type": {
"id": "00000000-0000-0000-0000-000000000004",
"name": "Supplier"
},
"sales_order_email": "order@example.com",
"updated_datetime": "2026-08-24T16:01:12.049718Z",
"website": null
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 6481bd8db4f1a34a8ca1977ae5987e0a-dd027dede6fdd861-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Returns 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.
Required permission: companies_permissions_view.
Request
GET /public/v1/companies/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the company to fetch. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single company | CompanyResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get companies
Success scenario
GET /public/v1/companies
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNGI1Y2YzYjctM2QwNS00ZTgwLThkNDMtMDQ3YWQyNzEzMjc4IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDA1IiwidHlwIjoiYWNjZXNzIn0.d18zGvgUisLaJeSnAbxGQ8qH_ZCIrjU1wD6A6qaupcI
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b8979e52a301c870bd6f3c85e15433eb-ef83b4f1255d85c8-0
{
"data": [
{
"category": "Retailer",
"custom_data": [
{
"id": 6,
"name": "Custom Field 5",
"value": "Custom Data 1"
}
],
"default_email": "company-1@example.com",
"default_payment_term": null,
"default_purchase_order_notes": "Default Purchase Order Notes 1",
"default_sales_order_notes": "Default Order External Notes 1",
"deleted_at": null,
"group": {
"id": "00000000-0000-0000-0000-00000000000b",
"name": "Comp Rel Group 9"
},
"id": "00000000-0000-0000-0000-000000000073",
"inserted_datetime": "2023-10-01T00:00:00.000000Z",
"invoice_email": "invoice email",
"leaflink_brand_id": 777,
"leaflink_customer_id": 555,
"legal_business_name": "Company Legal Name 1",
"licenses": [
{
"active": true,
"expiry_datetime": "2026-09-24T16:01:04.294120Z",
"id": "00000000-0000-0000-0000-000000000007",
"inserted_datetime": "2026-08-24T16:01:04.294224Z",
"issue_datetime": "2026-08-24T16:01:04.294118Z",
"license_number": "CDPH-00000008",
"license_type": "Specialty Cottage Indoor"
}
],
"locations": [
{
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000163",
"id": "00000000-0000-0000-0000-000000000046",
"license_id": null,
"name": "Place 69"
}
],
"name": "Company 354",
"order_shipment_email": "order shipment email",
"outstanding_balance": "0",
"outstanding_balance_threshold": 1000,
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner@example.com",
"full_name": "FirstName834 LastName835",
"id": "00000000-0000-0000-0000-000000000198",
"inserted_datetime": "2026-08-24T16:01:04.240690Z",
"role": {
"id": "00000000-0000-0000-0000-00000000019e",
"name": "Admin 413"
}
},
"owner_id": "00000000-0000-0000-0000-000000000198",
"phone_number": "1234567890",
"purchase_order_email": "purchase email",
"qb_customer_id": "QB-CUST-1",
"qb_vendor_id": "QB-VEND-1",
"relationship_type": {
"id": "00000000-0000-0000-0000-000000000001",
"name": "Supplier"
},
"sales_order_email": "order email",
"updated_datetime": "2023-11-03T00:00:00.000000Z",
"website": "https://www.example.com"
},
{
"category": "Lab",
"custom_data": [
{
"id": 6,
"name": "Custom Field 5",
"value": null
}
],
"default_email": "company-790@example.com",
"default_payment_term": null,
"default_purchase_order_notes": null,
"default_sales_order_notes": null,
"deleted_at": null,
"group": null,
"id": "00000000-0000-0000-0000-000000000075",
"inserted_datetime": "2023-10-02T00:00:00.000000Z",
"invoice_email": null,
"leaflink_brand_id": null,
"leaflink_customer_id": null,
"legal_business_name": "Company Legal Name 365",
"licenses": [
{
"active": true,
"expiry_datetime": "2026-09-24T16:01:04.315455Z",
"id": "00000000-0000-0000-0000-000000000008",
"inserted_datetime": "2026-08-24T16:01:04.315771Z",
"issue_datetime": "2026-08-24T16:01:04.315454Z",
"license_number": "CDPH-00000009",
"license_type": "Type 10 Retailer"
}
],
"locations": [],
"name": "Company 365",
"order_shipment_email": null,
"outstanding_balance": "0",
"outstanding_balance_threshold": null,
"owner": null,
"owner_id": null,
"phone_number": null,
"purchase_order_email": null,
"qb_customer_id": null,
"qb_vendor_id": null,
"relationship_type": null,
"sales_order_email": null,
"updated_datetime": "2023-12-02T00:00:00.000000Z",
"website": null
}
],
"next_page": null
}
Error scenario: invalid page number
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 15af03309952dcbc202f2153b8363750-377c15c825b9abb0-0
{
"errors": [
{
"context": {},
"message": "must be greater than 0",
"pointer": [
"page",
"number"
],
"section": "query"
}
]
}
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.
This 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.
Required 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.
Request
GET /public/v1/companies
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| category | 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.Other Cultivator Delivery Dispensary Distributor Lab Manufacturer Microbusiness Retail |
query | string | false | ?category=Dispensary | |
| city | Restrict to companies that have a location whose city matches this case-insensitive substring, matched against the city of each company's locations. |
query | string | false | ?city=beverly | |
| company_group_ids | 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. |
query | array | false | ?company_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| custom_data | 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. |
query | object | false | ?custom_data[101]=Blue&custom_data[102]=Wholesale | |
| deleted | 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.no include only |
query | string | false | no | |
| has_outstanding_balance | 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. |
query | boolean | false | ?has_outstanding_balance=true | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z, | |
| leaflink_customer_ids | 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. |
query | array | false | ?leaflink_customer_ids[]=12345 | |
| legal_business_name | 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. |
query | string | false | ?legal_business_name=acme%20llc | |
| license_number | 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. |
query | string | false | ?license_number=C11-0000123-LIC | |
| name | 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. |
query | string | false | ?name=acme | |
| names | 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. |
query | array | false | ?names[]=Acme%20Dispensary&names[]=Green%20Leaf | |
| outstanding_balance | 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. |
query | string | false | ?outstanding_balance=100,500 | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| qb_customer_ids | 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. |
query | array | false | ?qb_customer_ids[]=42 | |
| qb_vendor_ids | 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. |
query | array | false | ?qb_vendor_ids[]=57 | |
| relationship_type_ids | 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. |
query | array | false | ?relationship_type_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| states | 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.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 |
query | array | false | ?states[]=CA&states[]=NY | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of companies | Companies |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a company
Success scenario
POST /public/v1/companies
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiNDc2NDI1ODYtYTFiNC00OWMyLWI0MjMtMTIxNDBjOTQ3OGFhIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTE4OSIsInR5cCI6ImFjY2VzcyJ9.MllipZt3ZQmj7SnmD4Su7rBCBSPtZnJP5pgH33n8iFA
{
"id": "00000000-0000-0000-0000-000000000155",
"name": "Updated Name"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ce1df31137602d358b77794407cbe514-017943620b724bd3-0
{
"data": {
"category": "Delivery",
"custom_data": [],
"default_email": "company-2122@example.com",
"default_payment_term": null,
"default_purchase_order_notes": null,
"default_sales_order_notes": null,
"deleted_at": null,
"group": null,
"id": "00000000-0000-0000-0000-000000000155",
"inserted_datetime": "2026-08-24T16:01:07.102789Z",
"invoice_email": null,
"leaflink_brand_id": null,
"leaflink_customer_id": null,
"legal_business_name": "Company Legal Name 931",
"licenses": [
{
"active": true,
"expiry_datetime": "2026-09-24T16:01:07.092651Z",
"id": "00000000-0000-0000-0000-000000000027",
"inserted_datetime": "2026-08-24T16:01:07.092773Z",
"issue_datetime": "2026-08-24T16:01:07.092649Z",
"license_number": "CDPH-00000040",
"license_type": "Specialty Cottage Mixed-Light Tier 2"
}
],
"locations": [
{
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-0000000003a6",
"id": "00000000-0000-0000-0000-0000000000f5",
"license_id": null,
"name": "Place 244"
}
],
"name": "Updated Name",
"order_shipment_email": null,
"outstanding_balance": "0",
"outstanding_balance_threshold": null,
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-1203@example.com",
"full_name": "FirstName2446 LastName2447",
"id": "00000000-0000-0000-0000-0000000004b6",
"inserted_datetime": "2026-08-24T16:01:07.099527Z",
"role": {
"id": "00000000-0000-0000-0000-0000000004d6",
"name": "Admin 1237"
}
},
"owner_id": "00000000-0000-0000-0000-0000000004b6",
"phone_number": null,
"purchase_order_email": null,
"qb_customer_id": null,
"qb_vendor_id": null,
"relationship_type": null,
"sales_order_email": null,
"updated_datetime": "2026-08-24T16:01:07.129430Z",
"website": null
}
}
Error scenario: missing name
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 8cea9f9340155de8fcca5f661d32e3d9-a6ca712e98bbe220-0
{
"errors": [
{
"context": {
"uuid": "6979be51-ea5d-415e-861f-66f19862ee38-4c5b4a8e-5ac8-4bc3-8312-79bcf5d9f244"
},
"message": "Please enter your company's name",
"pointer": [
"name"
],
"section": "body"
}
]
}
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.
A 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.
Updates 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.
This 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.
You 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.
Validation 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.
Required 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.
Request
POST /public/v1/companies
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| category | 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. |
body | string | false | Dispensary | |
| custom_data | 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. |
body | object | false | {"101":"Some text value","102":"2026-08-18T00:00:00.000-07:00","103":["Option A","Option B"]} | |
| default_email | Primary email address for the company. Must be a valid email address when provided. Left as-is when omitted on update. | body | string | false | ||
| default_payment_term_id | 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. |
body | string | false | ||
| default_purchase_order_notes | Notes pre-filled onto new purchase orders created for this company. Left as-is when omitted on update. | body | string | false | ||
| default_sales_order_notes | Notes pre-filled onto new sales orders created for this company. Left as-is when omitted on update. | body | string | false | ||
| group_id | ID of the group to assign to this company (surfaces as the group object in responses). Left as-is when omitted on update. |
body | string | false | ||
| id | 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. | body | string | false | ||
| invoice_email | Email address that invoices for this company are sent to. Left as-is when omitted on update. | body | string | false | ||
| leaflink_customer_id | 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. |
body | integer | false | ||
| legal_business_name | The company's registered legal business name, distinct from its display name. Left as-is when omitted on update. |
body | string | false | ||
| name | 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. | body | string | false | Acme Dispensary | |
| order_shipment_email | Email address that order shipment notifications for this company are sent to. Left as-is when omitted on update. | body | string | false | ||
| outstanding_balance_threshold | 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. | body | integer | false | ||
| owner_id | 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. | body | string | false | ||
| phone_number | Phone number for the company. Free-text; not format-validated. Left as-is when omitted on update. | body | string | false | ||
| purchase_order_email | Email address that purchase orders for this company are sent to. Left as-is when omitted on update. | body | string | false | ||
| qb_customer_id | 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. |
body | string | false | ||
| qb_vendor_id | 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. |
body | string | false | ||
| relationship_type_id | 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. |
body | string | false | ||
| sales_order_email | Email address that sales orders for this company are sent to. Left as-is when omitted on update. | body | string | false | ||
| website | Website URL for the company. Left as-is when omitted on update. | body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | An updated company relationship | CompanyResponse |
| 201 | A new company relationship | CompanyResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
CompanyGroup
Delete a company group
Success scenario
DELETE /public/v1/company-groups/00000000-0000-0000-0000-000000000012
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNDc0NzhjZTQtMWYzYi00NWEzLWExYWItZjFjMTZhNjM5NjFiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NjE5IiwidHlwIjoiYWNjZXNzIn0.2LzrsJFhtFau5HKo_QhKMefHg62BQ7aO3BMmE8xia1w
Response
204
cache-control: max-age=0, private, must-revalidate
b3: f135638d54e2ae3f82d083f5edcc2987-dc6f32625dd817cf-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 183c2ce30977aa879cb2a555eb4a51fe-12b8dedc2ecbc148-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Permanently deletes a company group. This is a hard delete: the group is removed and cannot be recovered — there is no soft-delete or undo. Returns 404 if no group with that ID belongs to the authenticated company.
Deleting a group also strips it from any price tier that targeted or excluded companies by this group: those price tiers keep working but lose this group from their include/exclude filters. The group is only a label, so deleting it does not delete any customer or vendor relationship that was tagged with it — those companies simply lose the tag.
Required permission: settings_permissions_company_relationship_groups.
Request
DELETE /public/v1/company-groups/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the company group to delete, as returned by the list and upsert endpoints. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a company group
Success scenario
GET /public/v1/company-groups/00000000-0000-0000-0000-000000000008
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiZGRmYjUzMTAtNzIwOC00NzY0LWFmZjQtZTY2ZjQ5Nzk1OGNhIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTk3IiwidHlwIjoiYWNjZXNzIn0.3i9YzIy7k1bCwF8MDWs-ib7LMc4ip2qnSUTyJJsTH8w
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 150e9012c11a56a6410ad039a3d0470a-44a3222459409a65-0
{
"data": {
"id": "00000000-0000-0000-0000-000000000008",
"inserted_datetime": "2026-08-24T16:01:03.349916Z",
"name": "Key Accounts",
"updated_datetime": "2026-08-24T16:01:03.349916Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 632ee35c673a084afc48b53726e10ca7-c65de28fa0387830-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Fetches a single company group by its ID. Returns 404 if no group with that ID belongs to the authenticated company — a group owned by a different company is indistinguishable from one that does not exist.
Required permission: settings_permissions_company_relationship_groups.
Request
GET /public/v1/company-groups/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the company group to fetch, as returned by the list and upsert endpoints. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single company group | CompanyGroupFullResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get company groups
Success scenario
GET /public/v1/company-groups
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZGM0MmY4ZDgtNDI4My00NDliLWIwMzEtNTYzYjdmNmViYjcwIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODIiLCJ0eXAiOiJhY2Nlc3MifQ.9U_e-suaYxe9xhiqx62ARxPE7ZM3ewiK89fCaefA0n4
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cb8bd83189cfd670b84c839f4be3ca30-196805e2cf4a7c57-0
{
"data": [
{
"id": "00000000-0000-0000-0000-000000000001",
"inserted_datetime": "2026-08-24T16:01:02.466708Z",
"name": "CG1",
"updated_datetime": "2026-08-24T16:01:02.466708Z"
},
{
"id": "00000000-0000-0000-0000-000000000002",
"inserted_datetime": "2026-08-24T16:01:02.467662Z",
"name": "CG2",
"updated_datetime": "2026-08-24T16:01:02.467662Z"
},
{
"id": "00000000-0000-0000-0000-000000000003",
"inserted_datetime": "2026-08-24T16:01:02.467955Z",
"name": "CG3",
"updated_datetime": "2026-08-24T16:01:02.467955Z"
}
],
"next_page": "https://www.example.com/public/v1/company-groups?page[number]=2"
}
Error scenario: invalid page param
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 219b400de76eecd9dd720a038e045b32-2d2389d9f41795f2-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
Lists the company groups owned by the authenticated company. Company groups are the labels you use to organize your business relationships (your customers and vendors) — for example "Wholesale", "Retail", or a region — and price tiers can target or exclude companies by group.
Results are scoped to your company only; groups belonging to other companies are never returned.
They come back sorted by creation time, oldest first, and are paginated: read next_page from the
envelope and request that URL to fetch the following page (it is null on the last page).
Required permission: settings_permissions_company_relationship_groups.
Request
GET /public/v1/company-groups
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | Page to fetch, 1-based. Defaults to the first page when omitted. Example: ?page[number]=2. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of company groups | CompanyGroups |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a company group
Success scenario
POST /public/v1/company-groups
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiZjI3MmQxNGMtYzAwYy00MTgyLWJmY2ItNzQwNDE1NzE3MDhiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjU4IiwidHlwIjoiYWNjZXNzIn0.1fHqG6chJp0IsXHovEslY3GWiVy7byA-EMDTZ2Tixi4
{
"name": "Key Accounts"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 1e3d7f4e6f1ffeda6e3de57223b5c5c4-a998219f726f8910-0
{
"data": {
"id": "00000000-0000-0000-0000-000000000009",
"inserted_datetime": "2026-08-24T16:01:03.702191Z",
"name": "Key Accounts",
"updated_datetime": "2026-08-24T16:01:03.702191Z"
}
}
Error scenario: missing name
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 531fc0f2bb8451bc561d83f513779b56-66c77d97e1240d88-0
{
"errors": [
{
"context": {},
"message": "Enter a name for this company group",
"pointer": [
"name"
],
"section": "body"
}
]
}
Creates or renames a single company group in one endpoint. Pass id to rename the matching
existing group; omit id to create a new one. A create responds 201, an update responds 200.
Company groups are the labels you use to organize your business relationships (customers and
vendors); price tiers can target or exclude companies by group.
name is required and must be unique within your company — reusing a name already taken by
another of your groups is rejected with a 400. On update, name fully replaces the stored name;
there are no other editable fields, so this endpoint only ever sets the group's name.
Renaming a group does not change its ID, so any price tier that already targets or excludes this group keeps doing so under the new name. Creating a group does not attach it to any company relationship — membership is managed separately.
Required permission: settings_permissions_company_relationship_groups.
Request
POST /public/v1/company-groups
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | body | string | false | ||
| name | 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. |
body | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated company group | CompanyGroupFullResponse |
| 201 | The created company group | CompanyGroupFullResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Contact
Delete a contact
Success scenario
DELETE /public/v1/contacts/00000000-0000-0000-0000-000000000001
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzAsImlhdCI6MTc4NzU4NzI3MCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZjQyZGJkZWYtZDk4MS00NmYwLTliM2YtNzU3YzZjMDdhZGNhIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY5LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjI1NyIsInR5cCI6ImFjY2VzcyJ9.fgBXQMglARwzvdJmgWbCe7Sv3llVOIuoSsLYxZIY6So
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 5448ba9743f5da802811d6c7d10286db-5a5edcd33594b7bb-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ba814631d8868f824545ba38e52cf1fa-ff618aa77fb9cfae-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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).
A 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.
Required permission: contacts_permissions_delete, plus access to the contact under your team restrictions — a contact the authenticated user cannot access returns 403.
Request
DELETE /public/v1/contacts/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a contact
Success scenario
GET /public/v1/contacts/00000000-0000-0000-0000-000000000035
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzAsImlhdCI6MTc4NzU4NzI3MCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZjQyZGJkZWYtZDk4MS00NmYwLTliM2YtNzU3YzZjMDdhZGNhIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY5LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjI1NyIsInR5cCI6ImFjY2VzcyJ9.fgBXQMglARwzvdJmgWbCe7Sv3llVOIuoSsLYxZIY6So
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4668c578448dfb269edbc64017b6879e-dc0548ebaaf57af8-0
{
"data": {
"company": {
"id": "00000000-0000-0000-0000-0000000002c3"
},
"custom_data": [
{
"id": 83,
"name": "Custom Field 57",
"value": "Custom Data 1"
}
],
"deleted_at": null,
"description": null,
"driver_license_issuing_state": null,
"driver_license_number": null,
"email": null,
"first_name": "John",
"full_name": "John Doe",
"id": "00000000-0000-0000-0000-000000000035",
"inserted_datetime": "2026-08-24T16:01:10.328001Z",
"last_name": "Doe",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-2255@example.com",
"full_name": "FirstName4596 LastName4597",
"id": "00000000-0000-0000-0000-0000000008da",
"inserted_datetime": "2026-08-24T16:01:10.315290Z",
"role": {
"id": "00000000-0000-0000-0000-000000000906",
"name": "Admin 2309"
}
},
"phone_number": null,
"title": null,
"updated_datetime": "2026-08-24T16:01:10.328001Z",
"work_phone_number": null
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ac3d8e73981de4e7e4fe19c21f254d1c-cbad2d4cfa9a0c55-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Required permission: contacts_permissions_view.
Request
GET /public/v1/contacts/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the contact to fetch. Must belong to your company; returns 404 otherwise. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single contact | ContactResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get contacts
Success scenario
GET /public/v1/contacts
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiNzIzOTM3NmYtN2E4Ni00OWMyLThjMWMtYTNhMjZjOWRkYmZiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTUxIiwidHlwIjoiYWNjZXNzIn0.Z8I-SOUMn3_-Q1OXw4JE28uwXQ_8gQbrpJt9uhcOe2A
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cedc21b821c5ee03e33356793aaaecf9-25d2fd910268c23e-0
{
"data": [
{
"company": {
"id": "00000000-0000-0000-0000-000000000028"
},
"custom_data": [
{
"id": 2,
"name": "Custom Field 1",
"value": "Custom Data 1"
}
],
"deleted_at": null,
"description": "Description 1",
"driver_license_issuing_state": "CA",
"driver_license_number": "1234567890",
"email": "email1",
"first_name": "first",
"full_name": "first name1",
"id": "00000000-0000-0000-0000-000000000006",
"inserted_datetime": "2026-08-24T16:01:03.160753Z",
"last_name": "name1",
"owner": {
"banned": false,
"deleted_at": null,
"email": "contact-owner@example.com",
"full_name": "FirstName320 LastName321",
"id": "00000000-0000-0000-0000-00000000009c",
"inserted_datetime": "2026-08-24T16:01:03.145539Z",
"role": {
"id": "00000000-0000-0000-0000-00000000009a",
"name": "Admin 153"
}
},
"phone_number": "1234567890",
"title": null,
"updated_datetime": "2026-08-24T16:01:03.160753Z",
"work_phone_number": "1234567891"
},
{
"company": {
"id": "00000000-0000-0000-0000-000000000029"
},
"custom_data": [
{
"id": 2,
"name": "Custom Field 1",
"value": null
}
],
"deleted_at": null,
"description": "Description 2",
"driver_license_issuing_state": "NV",
"driver_license_number": "123456789",
"email": "email2",
"first_name": "first",
"full_name": "first name2",
"id": "00000000-0000-0000-0000-000000000007",
"inserted_datetime": "2026-08-24T16:01:03.175947Z",
"last_name": "name2",
"owner": {
"banned": false,
"deleted_at": null,
"email": "contact-owner@example.com",
"full_name": "FirstName320 LastName321",
"id": "00000000-0000-0000-0000-00000000009c",
"inserted_datetime": "2026-08-24T16:01:03.145539Z",
"role": {
"id": "00000000-0000-0000-0000-00000000009a",
"name": "Admin 153"
}
},
"phone_number": "1234567890",
"title": null,
"updated_datetime": "2026-08-24T16:01:03.175947Z",
"work_phone_number": "1234567892"
}
],
"next_page": null
}
Error scenario: invalid page number
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 0ffd74ad3476b818c465aa50a4acc18a-32129e93f61883c5-0
{
"errors": [
{
"context": {},
"message": "must be greater than 0",
"pointer": [
"page",
"number"
],
"section": "query"
}
]
}
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).
This 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.
By 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.
Required permission: contacts_permissions_view.
Request
GET /public/v1/contacts
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| company_ids | 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. |
query | array | false | ?company_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| custom_data | 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. |
query | object | false | ?custom_data[101]=Blue&custom_data[102]=Wholesale | |
| deleted | 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.no include only |
query | string | false | no | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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). |
query | string | false | 2022-07-10T00:00:00Z, | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of contacts | Contacts |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a contact
Success scenario
POST /public/v1/contacts
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNWNkNTgyNzQtMDdjOC00NjM1LTgxYTktNGY3ZmEyM2E5YzA2IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODgxIiwidHlwIjoiYWNjZXNzIn0.x_VTX6pGgFBmA5tW1MY-ONd6k03r5EUN-V-OZ7QYlwg
{
"company_id": "00000000-0000-0000-0000-0000000000fd",
"custom_data": {
"18": [
"VIP",
"Wholesale"
]
},
"description": "Notes",
"driver_license_issuing_state": "CA",
"driver_license_number": "D123",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"owner_id": "00000000-0000-0000-0000-000000000378",
"phone_number": "555-1111",
"title": "Buyer",
"work_phone_number": "555-2222"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 0e1abdfe3fa76d4948eb20fba54a985f-6fb726f4441bc393-0
{
"data": {
"company": {
"id": "00000000-0000-0000-0000-0000000000fd"
},
"custom_data": [
{
"id": 18,
"name": "Custom Field 15",
"value": "VIP,Wholesale"
}
],
"deleted_at": null,
"description": "Notes",
"driver_license_issuing_state": "CA",
"driver_license_number": "D123",
"email": "jane@example.com",
"first_name": "Jane",
"full_name": "Jane Doe",
"id": "00000000-0000-0000-0000-000000000015",
"inserted_datetime": "2026-08-24T16:01:06.071982Z",
"last_name": "Doe",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-885@example.com",
"full_name": "FirstName1802 LastName1803",
"id": "00000000-0000-0000-0000-000000000378",
"inserted_datetime": "2026-08-24T16:01:06.030796Z",
"role": {
"id": "00000000-0000-0000-0000-000000000391",
"name": "Admin 912"
}
},
"phone_number": "555-1111",
"title": "Buyer",
"updated_datetime": "2026-08-24T16:01:06.071982Z",
"work_phone_number": "555-2222"
}
}
Error scenario: missing full name
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 9667a95507622fe981104187df0e96cc-8e765c0f45ac13d4-0
{
"errors": [
{
"context": {
"uuid": "3f55f462-6e86-4abc-8d7a-888fab3d3e13"
},
"message": "Please enter a name",
"pointer": [
"first_name"
],
"section": "body"
},
{
"context": {
"uuid": "3f55f462-6e86-4abc-8d7a-888fab3d3e13"
},
"message": "Initials are required",
"pointer": [
"full_name"
],
"section": "body"
}
]
}
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.
Updates 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.
A 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).
Required 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.
Request
POST /public/v1/contacts
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| company_id | 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. |
body | string | false | ||
| custom_data | 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. | body | object | false | {"101":"Some text value","102":"2026-08-18T00:00:00.000-07:00","103":["Option A","Option B"]} | |
| description | Free-text note describing the contact. | body | string | false | ||
| driver_license_issuing_state | 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. |
body | string | false | ||
| driver_license_number | Driver license number recorded on shipping manifests when this contact is assigned as the driver on an order shipment. | body | string | false | ||
Email address of the contact. When provided, must contain an @. |
body | string | false | |||
| first_name | 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. |
body | string | true | ||
| id | 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. | body | string | false | ||
| last_name | Last name of the contact. When provided, must be 1-100 characters. Combined with first_name to form the response full_name. |
body | string | false | ||
| owner_id | 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. | body | string | false | ||
| phone_number | Primary phone number of the contact. | body | string | false | ||
| title | Job title of the contact. | body | string | false | ||
| work_phone_number | Work phone number of the contact, returned as work_phone_number. |
body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | An updated contact | ContactResponse |
| 201 | A new contact | ContactResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Cost
Add costs to batches
Success scenario
POST /public/v1/batches/add-costs
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTYyNmM3NzAtMDhkMy00ZTcwLWE1YjctMzk2M2ViODMzZGRiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6OTA0IiwidHlwIjoiYWNjZXNzIn0.xFiR3KLPJlrrpuuxMnpPNmZ3cQc-9MO3x-JBlPHpfdY
{
"batch_ids": [
"00000000-0000-0000-0000-00000000005a"
],
"costs": [
{
"cost_per_unit": 3,
"cost_type_id": "00000000-0000-0000-0000-000000000024",
"quantity": 2
}
]
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 0c67b06dca720fd78607b6cc22a49fcd-bbd30536c7659e19-0
{
"data": [
{
"batch_number": null,
"cbd": null,
"cost_per_unit_actual": "1.2",
"cost_per_unit_default": "0.8",
"creator": null,
"custom_data": [],
"deleted_at": null,
"description": null,
"expiration_date": null,
"expiration_datetime": null,
"harvest_datetime": null,
"id": "00000000-0000-0000-0000-00000000005a",
"inserted_datetime": "2026-08-24T16:01:06.156187Z",
"manufactured_datetime": "2026-08-24T16:01:06.036078Z",
"name": "B334",
"owner_id": "00000000-0000-0000-0000-00000000039a",
"primary_test_result": null,
"product": {
"id": "6aeb6328-88b2-4422-bd0a-0b64f3f63743",
"name": "Product 329",
"sku": "sku 330",
"updated_datetime": "2026-08-24T16:01:06.134754Z"
},
"product_id": "6aeb6328-88b2-4422-bd0a-0b64f3f63743",
"quantity_active": "0",
"quantity_active_by_location": [],
"thc": null,
"total_cost_actual": "6",
"total_cost_default": "4",
"updated_datetime": "2026-08-24T16:01:06.156187Z"
}
]
}
Error scenario: empty batch ids
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 3153e7f15596a272d8f3924096de6640-6d8e04e54ab290de-0
{
"errors": [
{
"context": {},
"message": "Cannot be an empty list",
"pointer": [
"batch_ids"
],
"section": "body"
}
]
}
Error scenario: locked cost type
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 63050a3ceeeb169c301ee86c2c7fb347-3542f46c50e048e5-0
{
"errors": [
{
"context": {
"id": "37b4176f-0473-4397-9409-308bc0c5c407"
},
"message": "This cost type doesn't allow inline editing cost per unit",
"pointer": [
"costs",
0,
"cost_per_unit"
]
}
]
}
Add one or more costs to each of the given batches, and return the affected batches with their updated cost totals.
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.
Each 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:
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.quantity(required): how many units of the cost type to apply, as a decimal. Must be greater than 0.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.description(optional): free-form text stored on the cost.
When 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.
This 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.
Adding 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.
Common errors (HTTP 400 unless noted):
- The
*_idslist is empty. - One or more ids don't exist or aren't accessible to the authenticated company.
- 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).
- A record has no quantity to add a cost to.
cost_type_idis missing or unknown, orquantityis missing or not greater than 0.cost_per_unitis set for a cost type with a locked cost per unit, or missing when required.distribute_by_quantityis used across records with mixed unit type categories.- Cost Accounting module is not enabled, or the user lacks
costs_permissions_apply_to_inventory(HTTP 403).
Required permission: costs_permissions_apply_to_inventory.
Request
POST /public/v1/batches/add-costs
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_ids | 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 | body | array(string) | true | ||
| costs | Required. Non-empty list of costs; each entry is applied to every listed batch | body | array(CostEntryInput) | true | ||
| distribute_by_quantity | 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 | body | boolean | false | ||
| location_ids | 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 | body | array(string) | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The affected batches | Batches |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Add costs to packages
Success scenario
POST /public/v1/packages/add-costs
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNWM1NmQwYjYtNjNlNi00OWZjLTg3YWUtYWNmMDUyZTY0MWQxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDQzIiwidHlwIjoiYWNjZXNzIn0.A6-uYBNlQ3H85vVp66Hm5lKxZ_Km9vtyIa1CoFaxi3w
{
"costs": [
{
"cost_type_id": "00000000-0000-0000-0000-000000000017",
"quantity": 4
}
],
"package_ids": [
"00000000-0000-0000-0000-000000000005"
]
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b554ed152d3993ec4bd62c9df8699c95-81d0d77bf957dc0a-0
{
"data": [
{
"batch_number": null,
"biotrack_id": null,
"biotrack_inventory_type_id": null,
"biotrack_net_quantity_per_unit": null,
"biotrack_room_id": null,
"biotrack_status": null,
"biotrack_usable_weight": null,
"compliance_label": "ABCDEF012345670000000008",
"compliance_product_name": "Buds",
"compliance_strain_name": "Cotton Candy",
"compliance_transferred_datetime": null,
"compliance_type": "METRC",
"cost_per_unit_actual": "2.666666666666666666666666667",
"cost_per_unit_default": "2.666666666666666666666666667",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-480@example.com",
"full_name": "FirstName982 LastName983",
"id": "00000000-0000-0000-0000-0000000001e2",
"inserted_datetime": "2026-08-24T16:01:04.473116Z",
"role": {
"id": "00000000-0000-0000-0000-0000000001ef",
"name": "Admin 494"
}
},
"custom_data": [],
"description": null,
"distru_status": "ACTIVE",
"expiration_date": null,
"expiration_datetime": null,
"finished_datetime": null,
"harvest_date": null,
"id": "00000000-0000-0000-0000-000000000005",
"inactivated_datetime": null,
"inserted_datetime": "2026-08-24T16:01:04.518776Z",
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": false,
"lab_testing_state": "NotSubmitted",
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:04.363518Z",
"id": "00000000-0000-0000-0000-00000000000a",
"inserted_datetime": "2026-08-24T16:01:04.363576Z",
"issue_datetime": "2026-08-24T16:01:04.363516Z",
"license_number": "CDPH-00000011",
"license_type": "Type 11 Distributor"
},
"license_id": "00000000-0000-0000-0000-00000000000a",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000172",
"id": "00000000-0000-0000-0000-00000000004b",
"license_id": "00000000-0000-0000-0000-00000000000a",
"name": "Place 74"
},
"location_id": "00000000-0000-0000-0000-00000000004b",
"metrc_archived_date": null,
"metrc_finished_date": null,
"metrc_id": 8,
"metrc_label": "ABCDEF012345670000000008",
"metrc_production_batch_number": null,
"metrc_received_datetime": null,
"metrc_received_from_manifest_number": null,
"metrc_source_harvest_names": null,
"metrc_status": "ACTIVE",
"metrc_transfer_id": null,
"metrc_unit_name": "Ounces",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-458@example.com",
"full_name": "FirstName938 LastName939",
"id": "00000000-0000-0000-0000-0000000001cc",
"inserted_datetime": "2026-08-24T16:01:04.411953Z",
"role": {
"id": "00000000-0000-0000-0000-0000000001d8",
"name": "Admin 471"
}
},
"packaged_date": "2014-11-29",
"primary_test_result": null,
"product": {
"id": "9c69631b-f796-420e-b4e0-651a96c30d26",
"name": "Product 163",
"sku": "sku 164",
"updated_datetime": "2026-08-24T16:01:04.444739Z"
},
"product_id": "9c69631b-f796-420e-b4e0-651a96c30d26",
"product_unit_quantity": "3.000000000",
"product_unit_type": {
"id": "00000000-0000-0000-0000-0000000011cf",
"name": "Ounce"
},
"quantity": "3.000000000",
"quantity_active": "3.000000000",
"quantity_assembling": "0.000000000",
"quantity_available": "3.000000000",
"status": "active",
"total_cost_actual": "8",
"total_cost_default": "8",
"unit_type": {
"id": "00000000-0000-0000-0000-0000000011cf",
"name": "Ounce"
}
}
]
}
Error scenario: empty package ids
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 52c9348b487ec66e0fbac3b784b2bee0-1948b42757b884e2-0
{
"errors": [
{
"context": {},
"message": "Cannot be an empty list",
"pointer": [
"package_ids"
],
"section": "body"
}
]
}
Error scenario: locked cost type
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 8c8b587651df3c30e5c4a4892f54c6d4-eb2d46bf26d200e4-0
{
"errors": [
{
"context": {
"id": "425baba8-a7f6-4b65-8dac-d88544dd08df"
},
"message": "This cost type doesn't allow inline editing cost per unit",
"pointer": [
"costs",
0,
"cost_per_unit"
]
}
]
}
Add one or more costs to each of the given packages, and return the affected packages with their updated cost totals.
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.).
Each 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:
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.quantity(required): how many units of the cost type to apply, as a decimal. Must be greater than 0.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.description(optional): free-form text stored on the cost.
When 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.
This 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.
Adding 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.
Common errors (HTTP 400 unless noted):
- The
*_idslist is empty. - One or more ids don't exist or aren't accessible to the authenticated company.
- 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).
- A record has no quantity to add a cost to.
cost_type_idis missing or unknown, orquantityis missing or not greater than 0.cost_per_unitis set for a cost type with a locked cost per unit, or missing when required.distribute_by_quantityis used across records with mixed unit type categories.- Cost Accounting module is not enabled, or the user lacks
costs_permissions_apply_to_inventory(HTTP 403).
Required permission: costs_permissions_apply_to_inventory.
Request
POST /public/v1/packages/add-costs
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| costs | Required. Non-empty list of costs; each entry is applied to every listed package | body | array(CostEntryInput) | true | ||
| distribute_by_quantity | 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 | body | boolean | false | ||
| package_ids | 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 | body | array(string) | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The affected packages | Packages |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Add costs to products
Success scenario
POST /public/v1/products/add-costs
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZmNiMzNjZTEtMTI1Mi00YmE4LWEyOGUtNTJhYzI0N2M3ODhmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTI2IiwidHlwIjoiYWNjZXNzIn0.neiR0oD7eSGx9piSh6oW1798IwgeuQTPvKD0X1MxcOY
{
"costs": [
{
"cost_type_id": "00000000-0000-0000-0000-00000000000e",
"quantity": 3
},
{
"cost_per_unit": 5,
"cost_type_id": "00000000-0000-0000-0000-00000000000e",
"quantity": 1
}
],
"product_ids": [
"bb76d5f2-b944-4c14-837e-34f3735310be"
]
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 742060e65e4d377e77a98ec2c5ae373d-cfebb800c454dac0-0
{
"data": [
{
"brand": null,
"category": {
"id": "00000000-0000-0000-0000-000000000013",
"name": "Some category 18",
"official_product_category_id": "OTHER"
},
"creator": null,
"custom_data": [],
"deleted_at": null,
"description": null,
"description_markdown": null,
"external_name": null,
"gross_weight": null,
"gross_weight_unit_type": null,
"id": "bb76d5f2-b944-4c14-837e-34f3735310be",
"images": [
{
"id": "00000000-0000-0000-0000-000000000001",
"name": "Image Name 6",
"rank": 0,
"url": "https://google.com/original-0.jpg"
}
],
"inserted_datetime": "2026-08-24T16:01:02.909588Z",
"inventory_tracking_method": "PRODUCT",
"is_active": true,
"is_featured": false,
"leaflink_product_id": null,
"menu_visibility": "DO_NOT_INCLUDE",
"menus": [
{
"id": "00000000-0000-0000-0000-000000000001",
"menu_id": "00000000-0000-0000-0000-000000000001",
"menu_name": "Menu 1",
"name": "Menu 1"
}
],
"msrp": null,
"name": "Product 44",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-127@example.com",
"full_name": "FirstName262 LastName263",
"id": "00000000-0000-0000-0000-00000000007f",
"inserted_datetime": "2026-08-24T16:01:02.880815Z",
"role": {
"id": "00000000-0000-0000-0000-000000000079",
"name": "Admin 120"
}
},
"product_group": {
"id": "00000000-0000-0000-0000-000000000015",
"name": "Product Group 20"
},
"quantity_active": "10",
"quantity_active_by_location": [
{
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000064",
"id": "00000000-0000-0000-0000-000000000029",
"license_id": null,
"name": "Place 40"
},
"quantity": "10"
}
],
"quantity_available": "6",
"quantity_available_threshold_max": null,
"quantity_available_threshold_min": null,
"quantity_reserved": "4",
"sku": "sku 45",
"strain": null,
"subcategory": {
"id": "00000000-0000-0000-0000-000000000015",
"name": "Some subcategory 20"
},
"tags": [
{
"id": "00000000-0000-0000-0000-000000000002",
"name": "Tag 1"
}
],
"total_cannabinoid_unit": null,
"total_cbd": null,
"total_thc": null,
"treez_wholesale_price": null,
"unit_cost": null,
"unit_net_weight": null,
"unit_net_weight_serving_size_unit_type": null,
"unit_price": "1",
"unit_serving_size": null,
"unit_type": {
"id": "00000000-0000-0000-0000-0000000004d4",
"name": "Gram"
},
"units_per_case": null,
"upc": null,
"updated_datetime": "2026-08-24T16:01:02.909588Z",
"vendor": {
"id": "00000000-0000-0000-0000-000000000023",
"name": "Company 103",
"updated_datetime": "2026-08-24T16:01:02.901352Z"
},
"wholesale_unit_price": null
}
]
}
Error scenario: empty product ids
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 3d6b103aa25479d1fb78e9268ec21164-700a3a8ff352c5ce-0
{
"errors": [
{
"context": {},
"message": "Cannot be an empty list",
"pointer": [
"product_ids"
],
"section": "body"
}
]
}
Error scenario: locked cost type
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4e3ea768fcbafea0831f78b12491e761-da3de07fb00503a2-0
{
"errors": [
{
"context": {
"id": "f244b307-b584-4b46-a830-59e9c292dca1"
},
"message": "This cost type doesn't allow inline editing cost per unit",
"pointer": [
"costs",
0,
"cost_per_unit"
]
}
]
}
Add one or more costs to each of the given product-tracked products, and return the affected products with their updated cost totals.
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.
Each 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:
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.quantity(required): how many units of the cost type to apply, as a decimal. Must be greater than 0.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.description(optional): free-form text stored on the cost.
When 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.
This 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.
Adding 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.
Common errors (HTTP 400 unless noted):
- The
*_idslist is empty. - One or more ids don't exist or aren't accessible to the authenticated company.
- 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).
- A record has no quantity to add a cost to.
cost_type_idis missing or unknown, orquantityis missing or not greater than 0.cost_per_unitis set for a cost type with a locked cost per unit, or missing when required.distribute_by_quantityis used across records with mixed unit type categories.- Cost Accounting module is not enabled, or the user lacks
costs_permissions_apply_to_inventory(HTTP 403).
Required permission: costs_permissions_apply_to_inventory.
Request
POST /public/v1/products/add-costs
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| costs | Required. Non-empty list of costs; each entry is applied to every listed product | body | array(CostEntryInput) | true | ||
| distribute_by_quantity | 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 | body | boolean | false | ||
| location_ids | 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 | body | array(string) | false | ||
| product_ids | 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 | body | array(string) | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The affected products | Products |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
CostType
Delete a cost type
Success scenario
DELETE /public/v1/cost-types/00000000-0000-0000-0000-00000000000f
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYWVmOTA3OTQtYmQxZC00MWEzLWI2ZDktY2VhYmNmZDg3YWUxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjMwIiwidHlwIjoiYWNjZXNzIn0.Ah-m4lVOVWKVe5xOYsXZE6UMNk8Upa-RF4194FrlSa0
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 5448ba9743f5da802811d6c7d10286db-5a5edcd33594b7bb-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ba814631d8868f824545ba38e52cf1fa-ff618aa77fb9cfae-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Soft-delete a cost type. After deletion it is excluded from the list and fetch endpoints and can no longer be applied to new records, but the row itself is retained. Returns 204 on success, or 404 if no cost type with that ID exists in your company — a cost type that was already deleted also reads as 404.
Deleting a cost type does not remove or alter costs already applied from it: those applied cost records snapshot their own amount and quantity, so they remain intact and unchanged. No inventory is affected and nothing is synced to Metrc or BioTrack. Because name uniqueness only considers active, non-deleted cost types, deleting a cost type frees its name to be reused by a new one.
Required permission: costs_permissions_manage_cost_types.
Request
DELETE /public/v1/cost-types/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the cost type to delete. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a cost type
Success scenario
GET /public/v1/cost-types/00000000-0000-0000-0000-000000000010
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiMTc3OGYzYWItMGE1Ni00YmRjLTg0MTktMmEyMjQ2MTdjZjY0IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjU3IiwidHlwIjoiYWNjZXNzIn0.w3I0Fhg3b77mBuKeJJaxO_ZbHNssjsSY78-ePoJOFHQ
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 2931aa16133e8bc003715ef63504e81f-f34be99cdf5effeb-0
{
"data": {
"active": true,
"allow_inline_edits": true,
"cost_per_unit": "25.5",
"deleted_at": null,
"description": null,
"id": "00000000-0000-0000-0000-000000000010",
"inserted_datetime": "2026-08-24T16:01:03.662817Z",
"name": "Freight",
"unit_type": {
"id": "00000000-0000-0000-0000-000000000ab5",
"name": "Unit Type 16"
},
"updated_datetime": "2026-08-24T16:01:03.662817Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 2cce166fe5832922ff18b4c2f62a3eb1-0c073e690781a6a6-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get a single cost type by ID. Returns 404 if no cost type with that ID exists in your company or if it has been soft-deleted. Inactive (but not deleted) cost types are still returned.
Required permission: costs_permissions_manage_cost_types.
Request
GET /public/v1/cost-types/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the cost type to fetch. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single cost type | CostTypeResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get cost types
Success scenario
GET /public/v1/cost-types
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYzc0ZTI5MjUtMzQ1Ny00MzM3LTk4ZDItNzc3MjRlYjhlZTBmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6OTkiLCJ0eXAiOiJhY2Nlc3MifQ.zzYj_8eQK2N6hk8u0uu18bFQfQ7gKZAb6smN-u-wc4o
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: af13e8cc22b01d242ae2be659f2ea0f0-9c46eb2ed1512616-0
{
"data": [
{
"active": true,
"allow_inline_edits": true,
"cost_per_unit": "1",
"deleted_at": null,
"description": null,
"id": "00000000-0000-0000-0000-000000000004",
"inserted_datetime": "2025-01-01T00:00:00.000000Z",
"name": "CT1",
"unit_type": {
"id": "00000000-0000-0000-0000-000000000400",
"name": "Unit Type 3"
},
"updated_datetime": "2026-08-24T16:01:02.687479Z"
},
{
"active": true,
"allow_inline_edits": true,
"cost_per_unit": "1",
"deleted_at": null,
"description": null,
"id": "00000000-0000-0000-0000-000000000005",
"inserted_datetime": "2025-01-02T00:00:00.000000Z",
"name": "CT2",
"unit_type": {
"id": "00000000-0000-0000-0000-000000000401",
"name": "Unit Type 4"
},
"updated_datetime": "2026-08-24T16:01:02.690653Z"
},
{
"active": true,
"allow_inline_edits": true,
"cost_per_unit": "1",
"deleted_at": null,
"description": null,
"id": "00000000-0000-0000-0000-000000000006",
"inserted_datetime": "2025-01-03T00:00:00.000000Z",
"name": "CT3",
"unit_type": {
"id": "00000000-0000-0000-0000-000000000402",
"name": "Unit Type 5"
},
"updated_datetime": "2026-08-24T16:01:02.693594Z"
}
],
"next_page": "https://www.example.com/public/v1/cost-types?page[number]=2"
}
Error scenario: invalid page param
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ed07874ea2b0a64234bd8ae6a19aaffc-3f8c7085190bea19-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
List cost types for the authenticated company. A cost type is a reusable, company-scoped category of cost accounting figure (e.g. freight, labor) with a default per-unit amount and a unit of measure. Cost types are configuration, not transactions — they are the templates you draw from when applying a cost to a plant, a package or batch, an assembly or breakdown output, a purchase item, or a product. Each applied cost snapshots its own amount and quantity at apply time, so a cost type's values here only seed future applications and never restate historical costs.
Results are scoped to your company and ordered oldest-first by creation time. Both active and
inactive cost types are returned; soft-deleted ones are excluded. The response is paginated —
follow the next_page URL to page through the full set.
Required permission: costs_permissions_manage_cost_types.
Request
GET /public/v1/cost-types
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of cost types | CostTypes |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a cost type
Success scenario
POST /public/v1/cost-types
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiNjkxNmJkMTktM2ZjZC00NDJiLWFhYmUtZDRlNDQxMTkwYTU4IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzM0IiwidHlwIjoiYWNjZXNzIn0.6nlF8a6xA1_xksmfcIjqQVr8B477chn49fzUjs3TSfM
{
"active": true,
"allow_inline_edits": true,
"cost_per_unit": "25.5",
"description": "Inbound shipping",
"name": "Freight",
"unit_type_id": "00000000-0000-0000-0000-000000000dc7"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 8f090954d7eb5a20d81e3b65e4460e40-55f0026d38186fbe-0
{
"data": {
"active": true,
"allow_inline_edits": true,
"cost_per_unit": "25.5",
"deleted_at": null,
"description": "Inbound shipping",
"id": "00000000-0000-0000-0000-000000000013",
"inserted_datetime": "2026-08-24T16:01:04.018298Z",
"name": "Freight",
"unit_type": {
"id": "00000000-0000-0000-0000-000000000dc7",
"name": "Unit Type 22"
},
"updated_datetime": "2026-08-24T16:01:04.018298Z"
}
}
Error scenario: missing name
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 394a556534b1d388f0913d6a6f58e6a8-c307efb0fa75c81b-0
{
"errors": [
{
"context": {},
"message": "This field is required",
"pointer": [
"name"
],
"section": "body"
}
]
}
Create or update a single cost type. This is one endpoint for both operations: omit id to
create a new cost type (responds 201), or pass the id of an existing cost type to update it
(responds 200). The URL and request shape are identical either way.
On create, name, cost_per_unit, unit_type_id and allow_inline_edits are all required.
On update the body is sparse — only the fields you send are changed, and any field you omit
keeps its current value. unit_type_id is immutable: once a cost type has a unit type it cannot
be reassigned, so sending a different unit_type_id on update is rejected. name must be unique
within your company, compared case-insensitively, among cost types that are both active and not
soft-deleted; inactive or deleted cost types do not reserve their name, so deactivating or
deleting a cost type frees its name for reuse. cost_per_unit must be non-zero (it may be
negative).
A cost type is configuration, not a transaction: this endpoint writes only the cost-type
template itself. It does not create, consume, reserve, or release inventory, and it never syncs
to Metrc or BioTrack — cost types have no compliance identity of their own. It also does not
touch costs already applied from this cost type: each applied cost (on a plant, a package or
batch, an assembly or breakdown output, a purchase item, or a product) snapshots its own amount
and quantity at apply time, so changing this cost type's cost_per_unit or allow_inline_edits
affects only future applications, never historical cost records.
Required permission: costs_permissions_manage_cost_types.
Request
POST /public/v1/cost-types
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| active | 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. | body | boolean | false | ||
| allow_inline_edits | 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. |
body | boolean | true | ||
| cost_per_unit | 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. |
body | string | true | ||
| 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. | body | string | false | ||
| id | 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. | body | string | false | ||
| name | 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. | body | string | true | ||
| unit_type_id | 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. | body | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated cost type | CostTypeResponse |
| 201 | The created cost type | CostTypeResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Credit
Cancel a credit
Success scenario
POST /public/v1/credits/17d132a5-0ba2-41d6-84e5-7255880501f8/cancel
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzAsImlhdCI6MTc4NzU4NzI3MCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZTNlYmNmZmEtYzI4YS00NTU4LTgxMjktNGU3MTYzODAyNjE2IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY5LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjI1NSIsInR5cCI6ImFjY2VzcyJ9.oG5RHaf65jn1XAMHmTRPBj0KKqui9A4OGixnI-bcvWc
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a8df15ead94dfbdc79527de189e46774-de106e047681b6e9-0
{
"data": {
"amount": "100",
"canceled_datetime": "2026-08-24T16:01:10.374069Z",
"company": {
"id": "00000000-0000-0000-0000-0000000002c5",
"name": "Company 1605",
"updated_datetime": "2026-08-24T16:01:10.329493Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-2262@example.com",
"full_name": "FirstName4610 LastName4611",
"id": "00000000-0000-0000-0000-0000000008e1",
"inserted_datetime": "2026-08-24T16:01:10.334243Z",
"role": {
"id": "00000000-0000-0000-0000-00000000090d",
"name": "Admin 2316"
}
},
"credit_number": "CRT-00000105",
"credit_uses": [
{
"amount": "40",
"credit": {
"amount": "100",
"credit_number": "CRT-00000105",
"id": "17d132a5-0ba2-41d6-84e5-7255880501f8",
"source": "USER"
},
"id": "1f99f92b-6ee4-46fa-a44f-fe420b69f72b",
"inserted_datetime": "2026-08-24T16:01:10.340557Z",
"payment": null
}
],
"deleted_in_qbo": false,
"external_note": "External note",
"id": "17d132a5-0ba2-41d6-84e5-7255880501f8",
"inserted_datetime": "2026-08-24T16:01:10.337707Z",
"internal_note": "Internal note",
"original_amount": "100",
"owner": null,
"payment": null,
"qb_credit_memo_id": null,
"qb_payment_id": null,
"qb_sync_status": null,
"remaining_balance": "60",
"return": null,
"source": "USER",
"status": "CANCELED",
"updated_datetime": "2026-08-24T16:01:10.374080Z"
}
}
Error scenario: cannot cancel overpayment credit
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a62514260e291ad21502621fb9d3b05a-edb30656a8fc9aad-0
{
"errors": [
{
"context": {
"id": "d20668dc-51d5-4bf0-9998-4baaa3162bc1"
},
"message": "Cannot cancel a credit created from an overpayment",
"pointer": [
"base"
]
}
]
}
Cancel (void) a credit. A canceled credit keeps its record and history and still appears in
this API, but its status becomes CANCELED and its remaining balance can no longer be applied
to invoices. The response returns the credit with its updated status.
By default the credit's existing applications to invoices (its credit uses) are left in place.
Set should_delete_credit_uses to true to also remove those applications, returning the used
amounts to the affected invoices and payments.
Overpayment credits (created from an invoice overpayment or a QuickBooks Online payment) cannot be canceled — void the associated payment instead, which returns an error here. Canceling an already-canceled credit is a no-op that returns the credit unchanged.
If your account syncs credits with QuickBooks Online, the cancellation is pushed to QuickBooks Online in the background; a 200 confirms the cancel in Distru, not that the QuickBooks Online side has finished. Credits do not touch inventory or state compliance (Metrc / BioTrack).
Required permission: credits_permissions_edit. The authenticated user must also have access to
the credit under their team restrictions.
Request
POST /public/v1/credits/{id}/cancel
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The credit's ID (as returned in the id field of a credit). |
path | string | true | ||
| should_delete_credit_uses | 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. | body | boolean | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The canceled credit | CreditResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Create or update a credit
Success scenario
POST /public/v1/credits
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZDhmNmY2YmEtZmMxNC00ZWM4LWI4ZTUtODAxOGQ3ZjIwNjFmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6Njc0IiwidHlwIjoiYWNjZXNzIn0.aX8ZA90ol9Hv9UXSRkpD-ksdewmKTVb2sbeUVVIArKU
{
"amount": 80,
"id": "6efeb7da-451e-41dd-a6f2-a6800d689664",
"internal_note": "updated"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ca95ecdd3274816726e6a8a2f8c8eec5-bf920ca079a81a60-0
{
"data": {
"amount": "80",
"canceled_datetime": null,
"company": {
"id": "00000000-0000-0000-0000-0000000000be",
"name": "Company 557",
"updated_datetime": "2026-08-24T16:01:05.234613Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-672@example.com",
"full_name": "FirstName1374 LastName1375",
"id": "00000000-0000-0000-0000-0000000002a2",
"inserted_datetime": "2026-08-24T16:01:05.211063Z",
"role": {
"id": "00000000-0000-0000-0000-0000000002b3",
"name": "Admin 690"
}
},
"credit_number": "CRT-0000001",
"credit_uses": [
{
"amount": "40",
"credit": {
"amount": "80",
"credit_number": "CRT-0000001",
"id": "6efeb7da-451e-41dd-a6f2-a6800d689664",
"source": "USER"
},
"id": "d8c7a58d-c070-4562-b054-46456d70cb3d",
"inserted_datetime": "2026-08-24T16:01:05.412985Z",
"payment": {
"amount": "10",
"company": {
"id": "00000000-0000-0000-0000-0000000000c9",
"name": "Company 589",
"updated_datetime": "2026-08-24T16:01:05.378444Z"
},
"credit_uses": [
{
"amount": "40",
"credit": {
"amount": "80",
"credit_number": "CRT-0000001",
"id": "6efeb7da-451e-41dd-a6f2-a6800d689664",
"source": "USER"
},
"id": "d8c7a58d-c070-4562-b054-46456d70cb3d"
}
],
"description": null,
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-000000000001",
"inserted_datetime": "2026-08-24T16:01:05.406119Z",
"invoice": {
"id": "00000000-0000-0000-0000-000000000009",
"invoice_number": "Invoice #8",
"status": "NOT_PAID",
"total": "32.00"
},
"overpayment_credits": [],
"payment_date": "2026-08-24T16:01:05.403493Z",
"payment_datetime": "2026-08-24T16:01:05.403493Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-00000000000f",
"inserted_datetime": "2026-08-24T16:01:05.401611Z",
"name": "Payment Method 14",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:05.401611Z"
},
"payment_number": "Payment #0",
"payment_type": "INVOICE",
"purchase": null,
"quickbooks_deposit_account_id": null,
"status": "POSTED",
"updated_datetime": "2026-08-24T16:01:05.406119Z"
}
}
],
"deleted_in_qbo": false,
"external_note": "ext",
"id": "6efeb7da-451e-41dd-a6f2-a6800d689664",
"inserted_datetime": "2026-08-24T16:01:05.313068Z",
"internal_note": "updated",
"original_amount": "150",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-672@example.com",
"full_name": "FirstName1374 LastName1375",
"id": "00000000-0000-0000-0000-0000000002a2",
"inserted_datetime": "2026-08-24T16:01:05.211063Z",
"role": {
"id": "00000000-0000-0000-0000-0000000002b3",
"name": "Admin 690"
}
},
"payment": null,
"qb_credit_memo_id": null,
"qb_payment_id": null,
"qb_sync_status": null,
"remaining_balance": "40",
"return": null,
"source": "USER",
"status": "ACTIVE",
"updated_datetime": "2026-08-24T16:01:05.463120Z"
}
}
Error scenario: customer required
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c00fc7454ddd9d1d2c844242d9cac979-8b911c8fe6018f0e-0
{
"errors": [
{
"context": {},
"message": "Please select a customer to apply this credit to",
"pointer": [
"company_id"
],
"section": "body"
}
]
}
Create a new credit or update an existing one through a single endpoint.
Omit id to create a new credit; include the id of an existing credit to update it. Updates
are sparse: only the fields you send are changed, and any field you omit keeps its current
value. On create, the credit_number and original_amount are assigned automatically —
original_amount is frozen to the create-time amount and never changes afterward.
Credits created through the API are always manually-created (USER source) credits — the same
as a credit you would add by hand in the Distru UI. Credits generated automatically (from a
return, an invoice overpayment, or QuickBooks Online) cannot be created here, and only
owner_id, external_note and internal_note can be updated on them — their amount, customer
and QuickBooks Online item cannot be set through the API. On a credit memo created in QuickBooks
Online (QB_CREDIT_MEMO source) owner_id is the only updatable field: its notes live in
QuickBooks Online and are re-imported from there on every sync.
Constraints: on update the customer (company_id) cannot be changed. amount must be greater
than 0 and, on update, cannot be set below the amount already applied to invoices by this credit
(its used amount). Once a credit has an owner it can be reassigned but not removed.
Credits do not touch inventory or state compliance (Metrc / BioTrack). They do interact with
QuickBooks Online: if your account syncs credits with QuickBooks Online, updating an existing
credit first pulls the latest credit and payment state from QuickBooks Online (so a stale local
amount can be rejected), and any create or update is then pushed to QuickBooks Online in the
background. A 200/201 confirms the credit was saved in Distru, not that it has finished syncing
to QuickBooks Online — re-fetch the credit and read qb_sync_status to observe the sync result.
Required permission: credits_permissions_create to create, credits_permissions_edit to
update. Updating also requires access to the credit under the authenticated user's team
restrictions, and an owner_id they can assign under those same restrictions.
Request
POST /public/v1/credits
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| amount | 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. |
body | number | false | ||
| company_id | 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. | body | string | false | ||
| external_note | A note on this credit, visible to the customer. Omit to leave unchanged on update; send null to clear. | body | string | false | ||
| id | 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. | body | string | false | ||
| internal_note | An internal note on this credit, not shown to the customer. Omit to leave unchanged on update; send null to clear. | body | string | false | ||
| owner_id | 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. | body | string | false | ||
| quickbooks_sales_item_id | 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. | body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated credit | CreditResponse |
| 201 | The created credit | CreditResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Delete a credit
Success scenario
DELETE /public/v1/credits/ea58ac40-08a6-41e3-8382-9c4f0b3023eb
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiM2Y3ODRmNWYtY2I4ZC00NzY0LWI2OTgtNGZjYWQ2ODkwYjRkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODU3IiwidHlwIjoiYWNjZXNzIn0.LHiGgEVjD3HkcXj7tGLEmxvzNsn-QfJhbZYbypxA0zU
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 27e3a1c33d4010bef9a5a70cb257904c-27c369764345a609-0
Error scenario: credit has been used
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 96e434ab27842ffc720d3ae507add484-6860ac50491d75ba-0
{
"errors": [
{
"context": {},
"message": "This credit has been used",
"pointer": [
"base"
]
}
]
}
Soft-delete a credit. The credit is marked as deleted and stops appearing in this API and the Distru UI, but the record is retained rather than being permanently removed, so its history is preserved. Returns 204 with no body on success.
A credit cannot be deleted once it has been used (has at least one active application to an invoice). Overpayment credits (created from an invoice overpayment or a QuickBooks Online payment) cannot be deleted unless they have already been canceled — void the associated payment instead. Either case returns an error.
If your account syncs credits with QuickBooks Online, the deletion is also pushed to QuickBooks Online in the background; a 204 confirms the delete in Distru, not that the QuickBooks Online side has finished.
Required permission: credits_permissions_delete. The authenticated user must also have access
to the credit under their team restrictions.
Request
DELETE /public/v1/credits/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The credit's ID (as returned in the id field of a credit). |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a credit
Success scenario
GET /public/v1/credits/92b25701-5fd4-4d40-b6ce-141f06608ec0
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjgsImlhdCI6MTc4NzU4NzI2OCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNGNhY2QxOGYtZDJiYS00ZmFjLTkzZTQtMTUzMGY5MmQwZTBhIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY3LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTc1NiIsInR5cCI6ImFjY2VzcyJ9.kJpASqL6emzC2mbaix3qUxO2I1UmbikIQQexyjA0WFs
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 769b575402a45b2ed5d26a1f6f3196bb-1022ab3cab97ff6f-0
{
"data": {
"amount": "100",
"canceled_datetime": null,
"company": {
"id": "00000000-0000-0000-0000-000000000221",
"name": "Company 1319",
"updated_datetime": "2026-08-24T16:01:08.902983Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1789@example.com",
"full_name": "FirstName3638 LastName3639",
"id": "00000000-0000-0000-0000-000000000702",
"inserted_datetime": "2026-08-24T16:01:08.909496Z",
"role": {
"id": "00000000-0000-0000-0000-000000000726",
"name": "Admin 1829"
}
},
"credit_number": "CRT-00000071",
"credit_uses": [
{
"amount": "25",
"credit": {
"amount": "100",
"credit_number": "CRT-00000071",
"id": "92b25701-5fd4-4d40-b6ce-141f06608ec0",
"source": "USER"
},
"id": "c97378a3-6d29-4bdc-8cb3-3813b8ffa2b2",
"inserted_datetime": "2026-08-24T16:01:08.917821Z",
"payment": {
"amount": "10",
"company": {
"id": "00000000-0000-0000-0000-000000000217",
"name": "Company 1303",
"updated_datetime": "2026-08-24T16:01:08.840083Z"
},
"credit_uses": [
{
"amount": "25",
"credit": {
"amount": "100",
"credit_number": "CRT-00000071",
"id": "92b25701-5fd4-4d40-b6ce-141f06608ec0",
"source": "USER"
},
"id": "c97378a3-6d29-4bdc-8cb3-3813b8ffa2b2"
}
],
"description": null,
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-000000000019",
"inserted_datetime": "2026-08-24T16:01:08.862861Z",
"invoice": {
"id": "00000000-0000-0000-0000-000000000022",
"invoice_number": "Invoice #32",
"status": "NOT_PAID",
"total": "32.00"
},
"overpayment_credits": [],
"payment_date": "2026-08-24T16:01:08.861641Z",
"payment_datetime": "2026-08-24T16:01:08.861641Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000027",
"inserted_datetime": "2026-08-24T16:01:08.860384Z",
"name": "Payment Method 38",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:08.860384Z"
},
"payment_number": "Payment #24",
"payment_type": "INVOICE",
"purchase": null,
"quickbooks_deposit_account_id": null,
"status": "POSTED",
"updated_datetime": "2026-08-24T16:01:08.862861Z"
}
}
],
"deleted_in_qbo": false,
"external_note": "External note",
"id": "92b25701-5fd4-4d40-b6ce-141f06608ec0",
"inserted_datetime": "2026-08-24T16:01:08.914098Z",
"internal_note": "Internal note",
"original_amount": "150",
"owner": null,
"payment": null,
"qb_credit_memo_id": null,
"qb_payment_id": null,
"qb_sync_status": null,
"remaining_balance": "75",
"return": null,
"source": "USER",
"status": "ACTIVE",
"updated_datetime": "2026-08-24T16:01:08.916395Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 960d34a489edc249f6f653427b3bc9af-ca55730d13413ef9-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get a single credit by its ID, scoped to your company. The response carries the credit's live
computed status and remaining_balance alongside its stored fields. Returns 404 if no such
credit exists for your company or it has been soft-deleted.
Required permission: credits_permissions_view. The authenticated user must also have access to
the requested credit under their team restrictions.
Request
GET /public/v1/credits/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The credit's ID (as returned in the id field of a credit). |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single credit | CreditResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get credits
Success scenario
GET /public/v1/credits
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNjVhZWE5NWQtZDExYi00YTdiLTkyMGMtYTJhN2RjZDk5NWVmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDEwIiwidHlwIjoiYWNjZXNzIn0.7iSSkadC5n0PwquSVMUyxNszcWZgH25F7kKS4n7pn7w
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 2391ef6a7cd0fbe5cedda0d66af3d1dd-8c43018ecb1064d7-0
{
"data": [
{
"amount": "100",
"canceled_datetime": null,
"company": {
"id": "00000000-0000-0000-0000-000000000071",
"name": "Company 355",
"updated_datetime": "2026-08-24T16:01:04.267341Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-421@example.com",
"full_name": "FirstName864 LastName865",
"id": "00000000-0000-0000-0000-0000000001a6",
"inserted_datetime": "2026-08-24T16:01:04.281565Z",
"role": {
"id": "00000000-0000-0000-0000-0000000001af",
"name": "Admin 430"
}
},
"credit_number": "CRT-A",
"credit_uses": [
{
"amount": "40",
"credit": {
"amount": "100",
"credit_number": "CRT-A",
"id": "78fd651b-7177-4f8b-95d4-ac23ccabf3a0",
"source": "USER"
},
"id": "b1a46cda-f9e4-4fb5-acff-bd0b8dd8f621",
"inserted_datetime": "2026-08-24T16:01:04.290324Z",
"payment": null
}
],
"deleted_in_qbo": false,
"external_note": "ext",
"id": "78fd651b-7177-4f8b-95d4-ac23ccabf3a0",
"inserted_datetime": "2026-08-24T16:01:04.286204Z",
"internal_note": "int",
"original_amount": "150",
"owner": null,
"payment": null,
"qb_credit_memo_id": null,
"qb_payment_id": null,
"qb_sync_status": null,
"remaining_balance": "60",
"return": null,
"source": "USER",
"status": "ACTIVE",
"updated_datetime": "2026-08-24T16:01:04.287796Z"
}
],
"next_page": null
}
Error scenario: invalid source filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 39384ca3309a013f675d419bbf00a12e-d4de215ad79a2325-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"source"
],
"section": "query"
}
]
}
List store credits held by your customers, newest first (by creation datetime, then id). A
credit is an amount a customer can apply toward what they owe; each row carries its live
computed status and remaining_balance.
Only credits belonging to your company are returned; soft-deleted credits are excluded. Filter
by customer (company_ids), by the invoices a credit has been applied to (invoice_ids), by
amount range, by source, status, credit_number, or the creation/last-modified windows.
All filters below are combined with AND — a credit must match every filter you send.
This endpoint returns eventually consistent data: a credit you just created, updated, canceled,
or deleted (and any change to its status/remaining_balance) can take up to 1 second to be
reflected here.
Required permission: credits_permissions_view. Only credits the authenticated user can access
under their team restrictions are returned.
Request
GET /public/v1/credits
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| amount | 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. |
query | string | false | ?amount=100,500 | |
| company_ids | 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. |
query | array | false | ?company_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| credit_number | Case-insensitive substring match on the credit number (partial matches count; e.g. 100 matches CR-1001). |
query | string | false | ?credit_number=CR-100 | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-01T00:00:00Z,2022-07-31T00:00:00Z | |
| invoice_ids | 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. |
query | array | false | ?invoice_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | Page number to fetch. Defaults to 1; must be greater than 0. |
query | number | false | ?page[number]=1 | |
| source | 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. INVOICE_PAYMENT QB_CREDIT_MEMO QB_PAYMENT RETURN USER |
query | string | false | ||
| status | 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. ACTIVE CANCELED REDEEMED |
query | string | false | ||
| updated_datetime | 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. |
query | string | false | ?updated_datetime=,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of credits | Credits |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
CustomField
Create a custom field
Success scenario
POST /public/v1/custom-fields
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZTQxZDQ5NzMtNjc0Zi00OWVhLWI5NzgtYzIwYWZmN2RjMDM3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NzQzIiwidHlwIjoiYWNjZXNzIn0.1ahBaNU7Oqw5VV_axaNYNtA6-8ywxZQBWkE7zbXgtEg
{
"field_options": [
"A",
"B"
],
"field_type": "dropdown",
"filterable": true,
"name": "Dropdown Field",
"parent_object": "product"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a4a0d2fae488fc45200942fa0caaa8d9-c4e4cc4448d9dcde-0
{
"data": {
"description": null,
"disabled_field_options": [],
"field_options": [
"A",
"B"
],
"field_type": "dropdown",
"filterable": true,
"id": 14,
"name": "Dropdown Field",
"parent_object": "product",
"required": false
}
}
Error scenario: name required
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 6fa621ac015e87a6bb903d274856c543-89ad78ac4b8aac82-0
{
"errors": [
{
"context": {},
"message": "can't be blank",
"pointer": [
"name"
],
"section": "body"
}
]
}
Create a new custom field on one entity type (parent_object).
Once 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.
This 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}.
Field-type rules the request must satisfy:
• 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.
• text and date fields must not carry field_options, and are always non-filterable regardless of what filterable is sent.
Required permission: settings_permissions_custom_fields.
Request
POST /public/v1/custom-fields
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| description | Optional free-text note describing the field's purpose. At most 100 characters. Omit or send null for no description. | body | string | false | ||
| disabled_field_options | 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. |
body | array | false | ||
| field_options | 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. |
body | array | false | ||
| field_type | 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.checkbox date dropdown text |
body | string | true | ||
| filterable | 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. |
body | boolean | false | ||
| name | 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. |
body | string | true | ||
| parent_object | 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. |
body | string | true | ||
| required | 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. | body | boolean | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 201 | Custom field created | CustomFieldDefinitionResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Delete a custom field
Success scenario
DELETE /public/v1/custom-fields/74
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzAsImlhdCI6MTc4NzU4NzI3MCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZjQyZGJkZWYtZDk4MS00NmYwLTliM2YtNzU3YzZjMDdhZGNhIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY5LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjI1NyIsInR5cCI6ImFjY2VzcyJ9.fgBXQMglARwzvdJmgWbCe7Sv3llVOIuoSsLYxZIY6So
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 5448ba9743f5da802811d6c7d10286db-5a5edcd33594b7bb-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ba814631d8868f824545ba38e52cf1fa-ff618aa77fb9cfae-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
A 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.
Required permission: settings_permissions_custom_fields.
Request
DELETE /public/v1/custom-fields/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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 |
path | integer | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a custom field definition
Success scenario
GET /public/v1/custom-fields/74
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjksImlhdCI6MTc4NzU4NzI2OSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNjljOTYyZGQtYmJlYi00NjI4LTg0NDYtNmJiOWM1ZDJlNDBjIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY4LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjA4OSIsInR5cCI6ImFjY2VzcyJ9.VYGSojx9hzALTW48LnVhyeJtMXz118osd1ULUM0-eZo
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 3e74e9d43f6fb667726affc6127a6f6c-cb1b1bf3cff1dfbd-0
{
"data": {
"description": "A test field",
"disabled_field_options": [
"B"
],
"field_options": [
"A",
"B",
"C"
],
"field_type": "dropdown",
"filterable": true,
"id": 74,
"name": "Test Field",
"parent_object": "product",
"required": false
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 60bf4a90b2e3baf462ca65055bfb17bb-ee3512cc5986ac83-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get one custom field definition by its numeric id, scoped to the authenticated company.
Use 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.
Required permission: settings_permissions_custom_fields.
Request
GET /public/v1/custom-fields/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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 |
path | integer | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single custom field definition | CustomFieldDefinitionResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
List custom field definitions
Success scenario
GET /public/v1/custom-fields
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZTgyYzViMjMtZGY0OS00MTcxLWI0Y2MtYzNlOGRlNDcwMTVlIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDg2IiwidHlwIjoiYWNjZXNzIn0.uH0DaXGWrkqjvtMGt1-Ha0TZ79cYC33UGdFoD23Qk1Y
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 189d5e47c792cdf6f51560479f6d4ac6-1fa6cc15e6b4c16a-0
{
"data": [
{
"description": null,
"disabled_field_options": [],
"field_options": [],
"field_type": "text",
"filterable": false,
"id": 8,
"name": "Field 1",
"parent_object": "product",
"required": false
},
{
"description": null,
"disabled_field_options": [
"A"
],
"field_options": [
"A",
"B"
],
"field_type": "dropdown",
"filterable": true,
"id": 9,
"name": "Field 2",
"parent_object": "product",
"required": false
}
]
}
List every custom field definition owned by the authenticated company.
A 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).
Pass 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.
Required permission: settings_permissions_custom_fields.
Request
GET /public/v1/custom-fields
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| parent_object | 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 |
query | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | List of custom field definitions | CustomFieldDefinitions |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Update a custom field
Success scenario
POST /public/v1/custom-fields/33
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTU1OGQ0YTYtNmU0ZS00MmJhLWFjZWItMDRkYmM3OWZjMWRkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTMyMCIsInR5cCI6ImFjY2VzcyJ9.JnMB1sYiDQc3yGmF3pQO2mc8_tq96b33xaelBLlpXZc
{
"name": "Updated Name"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: fb5ff3b60877c448a5e3622fcd9a0408-3ef81e6e2f4cd8e8-0
{
"data": {
"description": null,
"disabled_field_options": [
"Medium"
],
"field_options": [
"Large",
"Medium",
"Small"
],
"field_type": "dropdown",
"filterable": false,
"id": 33,
"name": "Updated Name",
"parent_object": "product",
"required": false
}
}
Error scenario: invalid field_options
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 774e9378ec82ffa43f70753ae5723203-c82c28179c6cdc42-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"field_options"
],
"section": "body"
}
]
}
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.
Each 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.
field_options is a full replacement of the option list, and editing it ripples through the whole company's data:
• 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.
• An option that stays in the list is preserved along with the records that selected it.
• A new value in the list is added as a selectable option.
• 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.
Omitting field_options entirely leaves the current options untouched. This field applies only to dropdown and checkbox fields.
This 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.
Required permission: settings_permissions_custom_fields.
Request
POST /public/v1/custom-fields/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| description | New description. Omit (or send null) to leave unchanged — null does not clear an existing description. At most 100 characters. | body | string | false | ||
| disabled_field_options | 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. |
body | array | false | ||
| field_options | 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. |
body | array | false | ||
| id | The numeric id of the custom field to update, as returned by the list endpoint. Example: 482 |
path | integer | true | ||
| name | 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. |
body | string | false | ||
| required | New value for whether a value must be supplied when a record is saved. Omit to leave unchanged. | body | boolean | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Custom field updated | CustomFieldDefinitionResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Driver
Delete a driver
Success scenario
DELETE /public/v1/drivers/00000000-0000-0000-0000-000000000012
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiY2JkNjkzYjItNjFjZS00YjkxLWExZGEtMTcxMzk3OWU4N2U2IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NjcwIiwidHlwIjoiYWNjZXNzIn0.UfVR1j5zzdJA95Pn13G_vjl9Ma87cbOVghpnF9Ovm2g
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 11f635608df3d2cf29a8dad8f659671f-d679b48782ca653c-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 52508656d67191f7ed0bcded3018cb2d-7f3c34ed9b48ce42-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Deletes a driver. This is a soft delete: the driver stops appearing in the list and fetch endpoints and can no longer be updated, but the record is retained. Returns 404 if no matching driver exists in your company or it was already deleted. Responds 204 with no body on success.
Deleting does not remove the driver from transfers, transfer templates, or manifests that already reference it. For a BIOTRACK company, a successful delete queues an asynchronous removal in BioTrack — a 204 means the driver was removed in Distru, not that BioTrack has processed the removal. The delete is rejected up front (and nothing is removed) 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. METRC companies have no such sync.
Required permission: settings_permissions_drivers.
Request
DELETE /public/v1/drivers/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the driver to delete, as returned by the list, fetch, and upsert endpoints. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a driver
Success scenario
GET /public/v1/drivers/00000000-0000-0000-0000-000000000009
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiNWYwYzM2ZTQtNTEzOC00NmJlLWI1NjgtNGZiOGQ3MDMyNzg3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjQwIiwidHlwIjoiYWNjZXNzIn0.8LaJYUy8HNiLRkSuizPyAzHwC2KUBXtCVz6LwPf0WII
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b3a688b5adf43b9bfb447b97529362f4-1b30c37faee64a05-0
{
"data": {
"birth_date": "1990-01-01",
"driver_license": "D1234567",
"email": "test@example.com",
"first_name": "Sam",
"hire_date": "2020-01-01",
"id": "00000000-0000-0000-0000-000000000009",
"inserted_datetime": "2026-08-24T16:01:03.615148Z",
"last_name": "Rivera",
"occupational_license_number": null,
"phone_number": null,
"updated_datetime": "2026-08-24T16:01:03.615148Z",
"us_state": "CA"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 34d61af1cbf709ead8c2776e3b6f043a-bbf0bcc318909307-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Fetches a single driver by its ID, scoped to the authenticated company. Returns 404 if no driver with that ID exists in your company or if it has been soft-deleted.
Which contact fields are populated depends on the company's compliance type: METRC drivers
carry phone_number and occupational_license_number (and leave birth_date, email,
us_state, hire_date null), while BIOTRACK drivers carry birth_date, email, us_state
and hire_date (and leave phone_number and occupational_license_number null).
Required permission: settings_permissions_drivers.
Request
GET /public/v1/drivers/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the driver to fetch, as returned by the list and upsert endpoints. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single driver | DriverResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get drivers
Success scenario
GET /public/v1/drivers
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOGEzNDlkOTktZGJhMC00MzEzLTk1NTQtOTY3M2Y0ZGFkZWRiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6OTUiLCJ0eXAiOiJhY2Nlc3MifQ.WJ6MemQE3SnF1JriaziNvZalwC3suzX5ckjQziS608Y
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b44f5516a7d337f1adde404710dc9583-19863a699cee22eb-0
{
"data": [
{
"birth_date": "1990-01-01",
"driver_license": "D1234567",
"email": "d1@example.com",
"first_name": "Test",
"hire_date": "2020-01-01",
"id": "00000000-0000-0000-0000-000000000002",
"inserted_datetime": "2025-01-01T00:00:00.000000Z",
"last_name": "Driver",
"occupational_license_number": null,
"phone_number": null,
"updated_datetime": "2026-08-24T16:01:02.671812Z",
"us_state": "CA"
},
{
"birth_date": "1990-01-01",
"driver_license": "D1234567",
"email": "d2@example.com",
"first_name": "Test",
"hire_date": "2020-01-01",
"id": "00000000-0000-0000-0000-000000000003",
"inserted_datetime": "2025-01-02T00:00:00.000000Z",
"last_name": "Driver",
"occupational_license_number": null,
"phone_number": null,
"updated_datetime": "2026-08-24T16:01:02.685638Z",
"us_state": "CA"
},
{
"birth_date": "1990-01-01",
"driver_license": "D1234567",
"email": "d3@example.com",
"first_name": "Test",
"hire_date": "2020-01-01",
"id": "00000000-0000-0000-0000-000000000004",
"inserted_datetime": "2025-01-03T00:00:00.000000Z",
"last_name": "Driver",
"occupational_license_number": null,
"phone_number": null,
"updated_datetime": "2026-08-24T16:01:02.695735Z",
"us_state": "CA"
}
],
"next_page": "https://www.example.com/public/v1/drivers?page[number]=2"
}
Error scenario: invalid page parameter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 77f1792d83078c4816073edc54c093bb-6db9aba4b2100ef1-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
Lists the drivers belonging to the authenticated company, oldest first (ordered by creation time ascending). Soft-deleted drivers are excluded, and results never cross company boundaries.
Drivers only exist for companies whose compliance type is METRC or BIOTRACK; a company with any other compliance type simply returns an empty list here.
Results are paginated. The response next_page holds the URL of the following page, or null on
the last page.
Required permission: settings_permissions_drivers.
Request
GET /public/v1/drivers
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of drivers | Drivers |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a driver
Success scenario
POST /public/v1/drivers
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTUzMzdlNTctMzg2My00OTM2LTkzYWQtYTEzNDMxYTEyMWJmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzYxIiwidHlwIjoiYWNjZXNzIn0.JBWw5vzsBwGc4j0byOwBTCDN22j_JgulLOukKwwBzTc
{
"driver_license": "D1234567",
"first_name": "Sam",
"last_name": "Rivera",
"occupational_license_number": "OCC-889",
"phone_number": "555-0100"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 26e5ec88adbf1cdfde3a6226bfbc8875-d603082e3048adc1-0
{
"data": {
"birth_date": null,
"driver_license": "D1234567",
"email": null,
"first_name": "Sam",
"hire_date": null,
"id": "00000000-0000-0000-0000-00000000000c",
"inserted_datetime": "2026-08-24T16:01:04.048492Z",
"last_name": "Rivera",
"occupational_license_number": "OCC-889",
"phone_number": "555-0100",
"updated_datetime": "2026-08-24T16:01:04.048492Z",
"us_state": null
}
}
Error scenario: invalid birth_date
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 96c1bc0338bf992e64701fa3cadf3b30-af86d6f671135a97-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"birth_date"
],
"section": "body"
}
]
}
Creates or updates a single driver in one call. Omit id to create a new driver; pass the
id of an existing driver to update it. Responds 201 on create and 200 on update, with
the full saved driver as the body.
Only companies whose compliance type is METRC or BIOTRACK can manage drivers. A request from a company with any other compliance type is rejected with a 400 and a single human-readable error message — no driver is created or changed.
Which fields are required depends on the company's compliance type, and the requirement is
enforced only when CREATING (no id). On update, every field is optional: this is a sparse
update, so only the fields you send are changed and any field you omit keeps its stored value.
Sending an empty string for an optional contact or license field (email, us_state,
driver_license, phone_number, occupational_license_number) stores null rather than an
empty string; leading and trailing whitespace on those fields is trimmed.
• METRC create requires: first_name, last_name, phone_number, driver_license,
occupational_license_number. The BIOTRACK-only fields (email, us_state, birth_date,
hire_date) are ignored for METRC companies.
• BIOTRACK create requires: first_name, last_name, birth_date, email, driver_license,
us_state, hire_date. The METRC-only fields (phone_number, occupational_license_number)
are ignored for BIOTRACK companies.
Compliance side effects: for a BIOTRACK company, a successful save queues an asynchronous push
to BioTrack — a 200/201 confirms the driver was stored in Distru, not that BioTrack accepted it,
so poll GET /public/v1/drivers/{id} to observe the stored record. The save is rejected up
front with a 400 (and nothing is stored) when your company or user BioTrack credentials are
missing or lack permission for this operation; once the push is queued, a later rejection by
BioTrack does not undo the Distru save. For a METRC company, the driver is stored for use when
building Metrc transfer templates and transfers and is not pushed to Metrc on save.
Required permission: settings_permissions_drivers.
Request
POST /public/v1/drivers
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| birth_date | 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. |
body | string | false | ||
| driver_license | The driver's license number. Required on create for both METRC and BIOTRACK companies. On update, omit to leave unchanged. | body | string | false | ||
| The driver's email. Required on create for BIOTRACK companies; ignored for METRC companies (stored null). On update, omit to leave unchanged. | body | string | false | |||
| first_name | The driver's first name. Required on create for every compliance type. On update, omit to leave unchanged. | body | string | false | ||
| hire_date | 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. |
body | string | false | ||
| id | 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. | body | string | false | ||
| last_name | The driver's last name. Required on create for every compliance type. On update, omit to leave unchanged. | body | string | false | ||
| occupational_license_number | The driver's occupational license number. Required on create for METRC companies; ignored for BIOTRACK companies (stored null). On update, omit to leave unchanged. | body | string | false | ||
| phone_number | 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. |
body | string | false | ||
| us_state | 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. |
body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated driver | DriverResponse |
| 201 | The created driver | DriverResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
FileAttachment
Insert a file attachment
Success scenario
POST /public/v1/file-attachments
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNWRlMzNiZDItMDFlZi00NzkyLTljZTQtZjBlZjVmNGM5MjZkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDA0IiwidHlwIjoiYWNjZXNzIn0.2um_gey2cSIVLK8eGcn5txLrrZVewihX9KSdZUXMEag
{
"file": {
"filename": "test-image.png",
"content_type": "image/png"
},
"name": "My Test Image",
"product_id": "118c9b59-ad40-4de7-a24b-644e5df4cbbb"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: fc640ea45377818c6d18f3500cbd8e82-d9407027a0f33542-0
{
"data": {
"assembly_id": null,
"batch_id": null,
"company_relationship_id": null,
"contact_id": null,
"id": "00000000-0000-0000-0000-000000000001",
"invoice_id": null,
"license_id": null,
"mime_type": "image/png",
"name": "My Test Image",
"order_id": null,
"order_shipment_id": null,
"product_id": "118c9b59-ad40-4de7-a24b-644e5df4cbbb",
"purchase_id": null,
"request_id": null,
"return_id": null,
"size_in_bytes": 355974,
"stock_transfer_id": null,
"task_id": null,
"upload_datetime": "2026-08-24T16:01:04.299604Z",
"uploader": {
"id": "00000000-0000-0000-0000-000000000194",
"name": "FirstName824 LastName827"
},
"url": "/var/folders/2z/jg98hkm57rx18c_x3bnqbr8c0000gn/T/9456ceca-dab9-46d5-8806-e67aedc67278/test-image.png"
}
}
Error scenario: empty file
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: bc539fe182cc42d239c5398e2672a4a3-802ade7db9597725-0
{
"errors": [
{
"context": {},
"message": "file cannot be empty",
"pointer": [
"file"
],
"section": "body"
}
]
}
Uploads a file and attaches it to exactly one existing record. On success the file is stored and
served back through the url on the response, so a single call both persists the document and
makes it downloadable.
Send the request as multipart/form-data with three parts:
• file — the binary to upload. Required, and must be non-empty (a zero-byte file is rejected).
• name — optional display name; defaults to the uploaded file's original filename when omitted or blank.
• 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.
This endpoint is create-only. There is no public endpoint to update or delete an attachment, and a file cannot be replaced once uploaded — upload a new attachment instead. The response echoes the attachment with all reference-id fields present but only the one you set populated; the rest are null.
The attachment is owned by the company tied to the API key you authenticate with, and the response's
uploader is the user that key belongs to. On success you get a 201 with the created attachment
wrapped in a data envelope; its url is immediately downloadable.
System effects: the upload counts against your company's storage quota. If it would exceed the remaining quota the call fails with 400 and nothing is stored — free space under Settings (or remove other attachments) and retry. This endpoint does not move inventory and does not push to Metrc or BioTrack; it only associates a document with the referenced record.
Required permission: products_permissions_edit.
Request
POST /public/v1/file-attachments
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| assembly_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| batch_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| company_relationship_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| contact_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| file | 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. |
formData | file | true | ||
| invoice_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| license_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| name | Display name for the attachment. Defaults to the uploaded file's original filename when omitted or blank. Max 255 characters. | formData | string | false | ||
| order_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| order_shipment_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| product_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| purchase_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| request_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| return_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| stock_transfer_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 | |
| task_id | 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. |
formData | string | false | 550e8400-e29b-41d4-a716-446655440000 |
Responses
| Status | Description | Schema |
|---|---|---|
| 201 | File attachment inserted successfully | FileAttachmentResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Inventory
Get inventory levels
Success scenario
GET /public/v1/inventory?groupings[]=PRODUCT&product_ids[]=b28cd142-cad8-48ec-a9e1-4619a1682f45
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiMGE5NjBhMDYtZTc3Ny00NGIyLTk1MTMtYzRlNTZjZjkxOWY5IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTI0NiIsInR5cCI6ImFjY2VzcyJ9.nT11NzCEywli0VRA39BH2pjMyUFXNa26SUn-czDzxhQ
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 6d1762911aa2925120df87fa73eb07df-fca51278604e136c-0
{
"data": [
{
"active": "10.000000000",
"available": "10.000000000",
"cost_default_per_unit": null,
"cost_per_unit_actual": null,
"cost_per_unit_default": null,
"product_id": "b28cd142-cad8-48ec-a9e1-4619a1682f45",
"reserved": "0.000000000",
"total_cost_actual": null,
"total_cost_default": null,
"updated_datetime": "2026-08-24T16:01:07.407164Z"
}
],
"next_page": null
}
Error scenario: groupings required
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 7252d16673c1e1d9e909f4ded72a0661-3198c04801130a3a-0
{
"errors": [
{
"context": {},
"message": "can't be blank",
"pointer": [
"groupings"
],
"section": "query"
}
]
}
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.
Results 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.
Groups 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.
Grouping by BATCH_NUMBER behaves specially:
• 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.
• reserved cannot be determined at the batch/package granularity, so it is always returned as "0" and available equals active for every row.
This 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.
Required permission: products_permissions_view.
Request
GET /public/v1/inventory
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_ids | 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. |
query | array | false | ?batch_ids[]=00000000-0000-0000-0000-000000000101&batch_ids[]=00000000-0000-0000-0000-000000000102 | |
| groupings | 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).PRODUCT LOCATION BATCH_NUMBER |
query | array | true | ?groupings[]=PRODUCT&groupings[]=LOCATION | |
| location_ids | 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. |
query | array | false | ?location_ids[]=00000000-0000-0000-0000-000000000001&location_ids[]=00000000-0000-0000-0000-000000000002 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| product_brand_ids | 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. | query | array | false | ?product_brand_ids[]=brand_abc&product_brand_ids[]=brand_def | |
| product_category_ids | 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. | query | array | false | ?product_category_ids[]=cat_abc&product_category_ids[]=cat_def | |
| product_group_ids | 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. | query | array | false | ?product_group_ids[]=grp_abc&product_group_ids[]=grp_def | |
| product_ids | 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). |
query | array | false | ?product_ids[]=67ae9080-8dc2-4ab7-9704-19673f4d9f21&product_ids[]=213c7080-8dc2-4ab7-9704-19673f4d9f22 | |
| product_skus | 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. | query | array | false | ?product_skus[]=SKU-001&product_skus[]=SKU-002 | |
| product_strain_ids | 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. | query | array | false | ?product_strain_ids[]=strain_abc&product_strain_ids[]=strain_def | |
| product_subcategory_ids | 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. | query | array | false | ?product_subcategory_ids[]=subcat_abc&product_subcategory_ids[]=subcat_def | |
| product_tag_ids | 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. | query | array | false | ?product_tag_ids[]=tag_abc&product_tag_ids[]=tag_def | |
| product_vendor_ids | 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. | query | array | false | ?product_vendor_ids[]=vendor_abc&product_vendor_ids[]=vendor_def |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of active and available quantity for each group | Inventories |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Invoice
Delete an invoice
Success scenario
DELETE /public/v1/invoices/00000000-0000-0000-0000-00000000004d
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzMsImlhdCI6MTc4NzU4NzI3MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmE0NTNlYjgtNjRkZS00NTNlLWI0NGEtOGE3ZWYxYTBjYzk1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzE0MiIsInR5cCI6ImFjY2VzcyJ9.prQ9oJP7X1X_OLC9F0AlSUp3TwgJHRCOSQn0mm-JP5o
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 8c2f5a1d94e3ba702811d6c7d10286db-4b7edcd33594b7aa-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cf914631d8868f824545ba38e52cf1ab-aa618aa77fb9cfbe-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Effects on payments and credits, all in one atomic call: every payment recorded on the invoice (voided ones included) is deleted with it. 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.
The 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.
Required permission: invoices_permissions_delete (plus access to the invoice under team restrictions).
Request
DELETE /public/v1/invoices/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get an invoice
Success scenario
GET /public/v1/invoices/00000000-0000-0000-0000-00000000004d
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzQsImlhdCI6MTc4NzU4NzI3NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNzJjNTBjMjEtYjM1NS00Zjk0LTk0ODYtNDc2ODBjMWU2MDhlIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjczLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzMyMSIsInR5cCI6ImFjY2VzcyJ9.savelIt13UEc1252ZvPDyju1Oux4aI8O5TXq3sQ0omg
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 5598d8563939deecf400a62a1a28a087-12115fb41de152e5-0
{
"data": {
"billing_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000935",
"id": "00000000-0000-0000-0000-0000000002ac",
"license_id": "00000000-0000-0000-0000-0000000000a3",
"license_number": "CDPH-00000166",
"name": "Place 683"
},
"charges": [
{
"id": "befa6605-368c-43cd-b280-7a32c16e9cc1",
"inserted_datetime": "2026-08-24T16:01:14.966571Z",
"name": "C1",
"percent": "10.0000",
"price": "1.00",
"tax": {
"id": "00000000-0000-0000-0000-000000000013",
"name": "T1"
},
"type": "CHARGE",
"unit_type": "PERCENT"
}
],
"company": {
"id": "00000000-0000-0000-0000-0000000004c7",
"name": "Company 2350",
"updated_datetime": "2026-08-24T16:01:14.752852Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-3307@example.com",
"full_name": "FirstName6712 LastName6713",
"id": "00000000-0000-0000-0000-000000000cfc",
"inserted_datetime": "2026-08-24T16:01:14.726055Z",
"role": {
"id": "00000000-0000-0000-0000-000000000d3a",
"name": "Admin 3385"
}
},
"custom_data": [],
"due_datetime": "2026-08-24T16:01:14.850638Z",
"external_notes": null,
"id": "00000000-0000-0000-0000-00000000004d",
"inserted_datetime": "2026-08-24T16:01:14.851303Z",
"internal_notes": null,
"invoice_datetime": "2026-08-24T16:01:14.850637Z",
"invoice_number": "Invoice #71",
"items": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000029c",
"name": "B2126"
},
"cost_per_unit": null,
"cost_per_unit_default": null,
"description": null,
"id": "00000000-0000-0000-0000-00000000002d",
"inserted_datetime": "2026-08-24T16:01:14.853350Z",
"order_item": {
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000029c",
"name": "B2126"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "e03c810d-6c99-437d-964e-5eaddcc04e7e",
"inserted_datetime": "2026-08-24T16:01:14.768034Z",
"is_sample": false,
"leaflink_id": null,
"location": null,
"note": null,
"package": null,
"price": "10.000000000",
"price_base": "10",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "b53694a7-572d-41a3-9507-cd600afca69a",
"name": "Product 2123",
"sku": "sku 2124",
"updated_datetime": "2026-08-24T16:01:14.764578Z"
},
"quantity": "15.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
},
"order_item_id": "e03c810d-6c99-437d-964e-5eaddcc04e7e",
"package": null,
"price": "10.000000000",
"product": {
"id": "b53694a7-572d-41a3-9507-cd600afca69a",
"name": "Product 2123",
"sku": "sku 2124",
"updated_datetime": "2026-08-24T16:01:14.764578Z"
},
"quantity": "10.000000000",
"returned_quantity": "0",
"total_cost_actual": null,
"total_cost_default": null
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000029e",
"name": "B2134"
},
"cost_per_unit": null,
"cost_per_unit_default": null,
"description": null,
"id": "00000000-0000-0000-0000-00000000002e",
"inserted_datetime": "2026-08-24T16:01:14.855423Z",
"order_item": {
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000029e",
"name": "B2134"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "e57ac616-666e-4dc1-97d3-3211e8604e33",
"inserted_datetime": "2026-08-24T16:01:14.786550Z",
"is_sample": false,
"leaflink_id": null,
"location": null,
"note": null,
"package": null,
"price": "10.000000000",
"price_base": "10",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "1d420139-e11e-4082-9c17-b32aa48b8d26",
"name": "Product 2132",
"sku": "sku 2133",
"updated_datetime": "2026-08-24T16:01:14.782821Z"
},
"quantity": "10.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
},
"order_item_id": "e57ac616-666e-4dc1-97d3-3211e8604e33",
"package": null,
"price": "10.000000000",
"product": {
"id": "1d420139-e11e-4082-9c17-b32aa48b8d26",
"name": "Product 2132",
"sku": "sku 2133",
"updated_datetime": "2026-08-24T16:01:14.782821Z"
},
"quantity": "10.000000000",
"returned_quantity": "0",
"total_cost_actual": null,
"total_cost_default": null
}
],
"order": {
"id": "e7500842-8e6d-4f3a-a55b-94c1c6313a43",
"order_number": "SO-117",
"status": "PENDING",
"total": "320.00"
},
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-3307@example.com",
"full_name": "FirstName6712 LastName6713",
"id": "00000000-0000-0000-0000-000000000cfc",
"inserted_datetime": "2026-08-24T16:01:14.726055Z",
"role": {
"id": "00000000-0000-0000-0000-000000000d3a",
"name": "Admin 3385"
}
},
"paid_amount": "5.00",
"payment_term_name": null,
"payments": [
{
"amount": "5",
"company": {
"id": "00000000-0000-0000-0000-0000000004c7",
"name": "Company 2350",
"updated_datetime": "2026-08-24T16:01:14.752852Z"
},
"credit_uses": [],
"description": null,
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-00000000002d",
"inserted_datetime": "2026-08-24T16:01:14.884737Z",
"invoice": {
"id": "00000000-0000-0000-0000-00000000004d",
"invoice_number": "Invoice #71",
"status": "PARTIALLY_PAID",
"total": "200.00"
},
"overpayment_credits": [],
"payment_date": "2026-08-24T16:01:14.866199Z",
"payment_datetime": "2026-08-24T16:01:14.866199Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-00000000003c",
"inserted_datetime": "2026-08-24T16:01:14.864836Z",
"name": "Payment Method 59",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:14.864836Z"
},
"payment_number": "PYT-0000001",
"payment_type": "INVOICE",
"purchase": null,
"quickbooks_deposit_account_id": null,
"status": "POSTED",
"updated_datetime": "2026-08-24T16:01:14.884737Z"
}
],
"remaining_amount": "195.00",
"status": "PARTIALLY_PAID",
"total": "200.00",
"updated_datetime": "2026-08-24T16:01:14.887440Z",
"voided_datetime": null
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: fbf1e8faa72dba3d3e4dcc0a12b6f6a3-2d01c28f1cb3a551-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Request
GET /public/v1/invoices/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The invoice's ID — the id returned by the list and show invoice endpoints. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | An invoice | InvoiceResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get invoices
Success scenario
GET /public/v1/invoices
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjksImlhdCI6MTc4NzU4NzI2OSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYWI0MWI3ZWUtZjAyMS00NzIwLThmZjgtYWZlMTAwNTMyOTMyIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY4LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTk3NSIsInR5cCI6ImFjY2VzcyJ9.jiJ1T_IvAn2ry8JQhEqglRr7i4m-0duJB4e6q7H2ZWo
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 5742ce6a152296646a4a7fa5dab00f52-1370895bbf5acfde-0
{
"data": [
{
"billing_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-0000000005f8",
"id": "00000000-0000-0000-0000-0000000001b2",
"license_id": "00000000-0000-0000-0000-000000000056",
"license_number": "CDPH-00000089",
"name": "Place 433"
},
"charges": [],
"company": {
"id": "00000000-0000-0000-0000-000000000294",
"name": "Company 1525",
"updated_datetime": "2026-08-24T16:01:09.960361Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-2111@example.com",
"full_name": "FirstName4306 LastName4307",
"id": "00000000-0000-0000-0000-00000000084a",
"inserted_datetime": "2026-08-24T16:01:09.896257Z",
"role": {
"id": "00000000-0000-0000-0000-000000000870",
"name": "Admin 2159"
}
},
"custom_data": [
{
"id": 73,
"name": "Custom Field 48",
"value": null
}
],
"due_datetime": "2026-08-24T16:01:10.164976Z",
"external_notes": null,
"id": "00000000-0000-0000-0000-000000000030",
"inserted_datetime": "2026-08-24T16:01:10.166043Z",
"internal_notes": null,
"invoice_datetime": "2026-08-24T16:01:10.164973Z",
"invoice_number": "Invoice #46",
"items": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000144",
"name": "B962"
},
"cost_per_unit": null,
"cost_per_unit_default": null,
"description": null,
"id": "00000000-0000-0000-0000-00000000000b",
"inserted_datetime": "2026-08-24T16:01:10.170095Z",
"order_item": {
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000144",
"name": "B962"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "aad497a8-d65d-43fa-a70d-1489ac251898",
"inserted_datetime": "2026-08-24T16:01:09.998577Z",
"is_sample": false,
"leaflink_id": null,
"location": null,
"note": null,
"package": null,
"price": "10.000000000",
"price_base": "10",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "412c9e8b-e2bf-48ce-8968-8265d580a359",
"name": "Product 959",
"sku": "sku 960",
"updated_datetime": "2026-08-24T16:01:09.989073Z"
},
"quantity": "15.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
},
"order_item_id": "aad497a8-d65d-43fa-a70d-1489ac251898",
"package": null,
"price": "10.000000000",
"product": {
"id": "412c9e8b-e2bf-48ce-8968-8265d580a359",
"name": "Product 959",
"sku": "sku 960",
"updated_datetime": "2026-08-24T16:01:09.989073Z"
},
"quantity": "10.000000000",
"returned_quantity": "0",
"total_cost_actual": null,
"total_cost_default": null
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000145",
"name": "B967"
},
"cost_per_unit": null,
"cost_per_unit_default": null,
"description": null,
"id": "00000000-0000-0000-0000-00000000000c",
"inserted_datetime": "2026-08-24T16:01:10.173636Z",
"order_item": {
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000145",
"name": "B967"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "48390973-d370-4817-86dd-76b64abebc84",
"inserted_datetime": "2026-08-24T16:01:10.032548Z",
"is_sample": false,
"leaflink_id": null,
"location": null,
"note": null,
"package": null,
"price": "10.000000000",
"price_base": "10",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "417ff518-8f06-4fb0-bd41-de344a8e6ee7",
"name": "Product 965",
"sku": "sku 966",
"updated_datetime": "2026-08-24T16:01:10.025745Z"
},
"quantity": "10.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
},
"order_item_id": "48390973-d370-4817-86dd-76b64abebc84",
"package": null,
"price": "10.000000000",
"product": {
"id": "417ff518-8f06-4fb0-bd41-de344a8e6ee7",
"name": "Product 965",
"sku": "sku 966",
"updated_datetime": "2026-08-24T16:01:10.025745Z"
},
"quantity": "10.000000000",
"returned_quantity": "0",
"total_cost_actual": null,
"total_cost_default": null
}
],
"order": {
"id": "10bf1a38-d065-4539-8c35-a36f76832b76",
"order_number": "SO-74",
"status": "PENDING",
"total": "320.00"
},
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-2111@example.com",
"full_name": "FirstName4306 LastName4307",
"id": "00000000-0000-0000-0000-00000000084a",
"inserted_datetime": "2026-08-24T16:01:09.896257Z",
"role": {
"id": "00000000-0000-0000-0000-000000000870",
"name": "Admin 2159"
}
},
"paid_amount": "0.0",
"payment_term_name": null,
"payments": [],
"remaining_amount": "200.00",
"status": "NOT_PAID",
"total": "200.00",
"updated_datetime": "2026-08-24T16:01:10.166043Z",
"voided_datetime": null
},
{
"billing_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-0000000005b0",
"id": "00000000-0000-0000-0000-000000000197",
"license_id": "00000000-0000-0000-0000-000000000054",
"license_number": "CDPH-00000087",
"name": "Place 406"
},
"charges": [
{
"id": "13161d72-de13-4875-8af9-5a85d94a70cb",
"inserted_datetime": "2026-08-24T16:01:09.889444Z",
"name": "C1",
"percent": "10.0000",
"price": "1.00",
"tax": {
"id": "00000000-0000-0000-0000-000000000012",
"name": "T1"
},
"type": "CHARGE",
"unit_type": "PERCENT"
}
],
"company": {
"id": "00000000-0000-0000-0000-00000000026e",
"name": "Company 1453",
"updated_datetime": "2026-08-24T16:01:09.551380Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "user1@a.com",
"full_name": "John Foo",
"id": "00000000-0000-0000-0000-00000000076e",
"inserted_datetime": "2026-08-24T16:01:09.243018Z",
"role": {
"id": "00000000-0000-0000-0000-000000000792",
"name": "Admin 1937"
}
},
"custom_data": [
{
"id": 73,
"name": "Custom Field 48",
"value": "Custom Field Value 1"
}
],
"due_datetime": "2020-01-01T00:00:01.000000Z",
"external_notes": "Visible to the customer",
"id": "00000000-0000-0000-0000-00000000002c",
"inserted_datetime": "2026-08-24T16:01:09.622458Z",
"internal_notes": "Only visible internally",
"invoice_datetime": "2020-01-01T00:00:02.000000Z",
"invoice_number": "INV-123",
"items": [
{
"batch": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"description": "Line description",
"id": "00000000-0000-0000-0000-00000000000a",
"inserted_datetime": "2026-08-24T16:01:09.626786Z",
"order_item": {
"batch": null,
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "3975691b-6dcd-472c-a369-8cfa68f0ad75",
"inserted_datetime": "2026-08-24T16:01:09.562000Z",
"is_sample": false,
"leaflink_id": null,
"location": null,
"note": null,
"package": {
"batch_number": "B1",
"compliance_label": "ABCDEF012345670000000096",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-000000000035",
"license_id": "00000000-0000-0000-0000-000000000050",
"location_id": "00000000-0000-0000-0000-00000000018d",
"metrc_id": 95,
"metrc_label": "ABCDEF012345670000000096",
"quantity": "10.000000000",
"quantity_active": "10.000000000",
"status": "active"
},
"price": "10.000000000",
"price_base": "10",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "a94a4591-68c4-489d-8eb4-c643d05d7acc",
"name": "P1",
"sku": "SKU1",
"updated_datetime": "2026-08-24T16:01:09.465488Z"
},
"quantity": "2.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
},
"order_item_id": "3975691b-6dcd-472c-a369-8cfa68f0ad75",
"package": {
"batch_number": "B1",
"compliance_label": "ABCDEF012345670000000096",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-000000000035",
"license_id": "00000000-0000-0000-0000-000000000050",
"location_id": "00000000-0000-0000-0000-00000000018d",
"metrc_id": 95,
"metrc_label": "ABCDEF012345670000000096",
"quantity": "10.000000000",
"quantity_active": "10.000000000",
"status": "active"
},
"price": "10.000000000",
"product": {
"id": "a94a4591-68c4-489d-8eb4-c643d05d7acc",
"name": "P1",
"sku": "SKU1",
"updated_datetime": "2026-08-24T16:01:09.465488Z"
},
"quantity": "1.000000000",
"returned_quantity": "0",
"total_cost_actual": null,
"total_cost_default": null
}
],
"order": {
"id": "f5fa3914-a21f-4903-a257-e3bb2712ec10",
"order_number": "SO-123",
"status": "COMPLETED",
"total": "10.00"
},
"owner": {
"banned": false,
"deleted_at": null,
"email": "user2@a.com",
"full_name": "John Bar",
"id": "00000000-0000-0000-0000-0000000007b4",
"inserted_datetime": "2026-08-24T16:01:09.414128Z",
"role": {
"id": "00000000-0000-0000-0000-0000000007d9",
"name": "Admin 2008"
}
},
"paid_amount": "5.00",
"payment_term_name": "Net 30",
"payments": [
{
"amount": "5",
"company": {
"id": "00000000-0000-0000-0000-00000000026e",
"name": "Company 1453",
"updated_datetime": "2026-08-24T16:01:09.551380Z"
},
"credit_uses": [],
"description": null,
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-000000000022",
"inserted_datetime": "2026-08-24T16:01:09.716967Z",
"invoice": {
"id": "00000000-0000-0000-0000-00000000002c",
"invoice_number": "INV-123",
"status": "PARTIALLY_PAID",
"total": "8.00"
},
"overpayment_credits": [],
"payment_date": "2026-08-24T16:01:09.665363Z",
"payment_datetime": "2026-08-24T16:01:09.665363Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-00000000002f",
"inserted_datetime": "2026-08-24T16:01:09.663941Z",
"name": "Payment Method 46",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:09.663941Z"
},
"payment_number": "PYT-0000001",
"payment_type": "INVOICE",
"purchase": null,
"quickbooks_deposit_account_id": null,
"status": "POSTED",
"updated_datetime": "2026-08-24T16:01:09.716967Z"
}
],
"remaining_amount": "3.00",
"status": "PARTIALLY_PAID",
"total": "8.00",
"updated_datetime": "2026-08-24T16:01:09.724468Z",
"voided_datetime": null
}
],
"next_page": null
}
Error scenario: invalid status filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 7496af6ce2bac339708c3150d4121ac6-707422e1001c2f90-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"statuses"
],
"section": "query"
}
]
}
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.
Results 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.
This 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.
Required 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.
Request
GET /public/v1/invoices
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_batch_numbers | 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). | query | array | false | ?batch_batch_numbers[]=B-1001 | |
| batch_ids | 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). | query | array | false | ?batch_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| company_group_ids | 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. | query | array | false | ?company_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| company_ids | 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. |
query | array | false | ?company_ids[]=550e8400-e29b-41d4-a716-446655440000&company_ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| custom_data | 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. |
query | object | false | ?custom_data[101]=Blue&custom_data[102]=Wholesale | |
| due_datetime | 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. |
query | string | false | ,2022-07-10T00:00:00Z | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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). |
query | string | false | 2022-07-10T00:00:00Z,2022-07-11T00:00:00Z | |
| invoice_datetime | 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). |
query | string | false | 2022-07-10T00:00:00Z, | |
| invoice_number | 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. |
query | string | false | 001 | |
| invoice_numbers | 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. |
query | array | false | ?invoice_numbers[]=1042&invoice_numbers[]=1043 | |
| is_voided | 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. |
query | boolean | false | ?is_voided=true | |
| order_ids | 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. |
query | array | false | ?order_ids[]=67ae9080-8dc2-4ab7-9704-19673f4d9f21&order_ids[]=213c7080-8dc2-4ab7-9704-19673f4d9f22 | |
| order_numbers | 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. |
query | array | false | ?order_numbers[]=SO-1042&order_numbers[]=SO-1043 | |
| order_statuses | 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.PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
query | array | false | ?order_statuses[]=COMPLETED&order_statuses[]=DELIVERED | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| package_batch_numbers | 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). | query | array | false | ?package_batch_numbers[]=B-1001 | |
| package_compliance_labels | 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). | query | array | false | ?package_compliance_labels[]=1A4FF0100000022000000123 | |
| package_ids | 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). | query | array | false | ?package_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| product_brand_ids | 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). | query | array | false | ?product_brand_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_category_ids | 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). | query | array | false | ?product_category_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_group_ids | 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). | query | array | false | ?product_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_ids | 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). | query | array | false | ?product_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_skus | 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). | query | array | false | ?product_skus[]=SKU-1001&product_skus[]=SKU-1002 | |
| product_strain_ids | 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). | query | array | false | ?product_strain_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_subcategory_ids | 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). | query | array | false | ?product_subcategory_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_tag_ids | 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). | query | array | false | ?product_tag_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_vendor_ids | 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). | query | array | false | ?product_vendor_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| remaining_amount | 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. |
query | string | false | 0.01, | |
| statuses | 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:
NOT_PAID PARTIALLY_PAID FULLY_PAID OVER_PAID |
query | array | false | ?statuses[]=NOT_PAID&statuses[]=OVER_PAID | |
| total | 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. |
query | string | false | 100,500 | |
| updated_datetime | 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). |
query | string | false | ,2022-07-10T00:00:00Z | |
| voided_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z, |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of invoices | Invoices |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Insert a payment for an invoice
Success scenario
POST /public/v1/invoices/00000000-0000-0000-0000-00000000008a/payments
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4ODMsImlhdCI6MTc4NzU4NzI4MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmZiYzA2YmUtMWFkZi00NWJiLWJiNGItNjE3NTFjNTVjZGJiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjgyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDcxOSIsInR5cCI6ImFjY2VzcyJ9.gQAkcPbox8XE38_HWVLzF3Zu4DfIVzye9h_KL_EjUY0
{
"amount": 100.01,
"description": "Payment for invoice",
"payment_datetime": "2020-01-01T00:00:00.000000Z",
"payment_method_id": "00000000-0000-0000-0000-000000000044",
"quickbooks_deposit_account_id": "QBD-123"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 9daee175fd36f1c0a4412e675d642392-c7644d44f163e899-0
{
"data": {
"amount": "100",
"company": {
"id": "00000000-0000-0000-0000-000000000802",
"name": "Company 3409",
"updated_datetime": "2026-08-24T16:01:23.830407Z"
},
"credit_uses": [],
"description": "Payment for invoice",
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-000000000036",
"inserted_datetime": "2026-08-24T16:01:23.903706Z",
"invoice": {
"id": "00000000-0000-0000-0000-00000000008a",
"invoice_number": "Invoice #127",
"status": "OVER_PAID",
"total": "100.00"
},
"overpayment_credits": [
{
"amount": "0.01",
"credit_number": "CRT-0000001",
"id": "282db989-21fd-4cc1-9738-0a8a2d26fed0",
"source": "INVOICE_PAYMENT"
}
],
"payment_date": "2020-01-01T00:00:00.000000Z",
"payment_datetime": "2020-01-01T00:00:00.000000Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000044",
"inserted_datetime": "2026-08-24T16:01:23.847174Z",
"name": "Payment Method 0",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:23.847174Z"
},
"payment_number": "PYT-0000001",
"payment_type": "INVOICE",
"purchase": null,
"quickbooks_deposit_account_id": "QBD-123",
"quickbooks_deposit_account_name": "QBD-NAME",
"quickbooks_sync_enqueued": true,
"status": "POSTED",
"updated_datetime": "2026-08-24T16:01:23.903706Z"
}
}
Error scenario: invalid payment fields
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 9bb0c55acf1e017a00d0191587baf4a6-23757f8156914839-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"payment_datetime"
]
},
{
"context": {},
"message": "is invalid",
"pointer": [
"payment_method_id"
]
}
]
}
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.
The 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.
If 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.
Required permission: invoices_permissions_receive_payment. The authenticated user must also be allowed to view invoices under their team restrictions.
Request
POST /public/v1/invoices/{id}/payments
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| amount | 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. | body | decimal | true | ||
| description | A free-text description of the payment. Required. | body | string | true | ||
| id | The id of the invoice to record the payment against — the id returned by the list and show invoice endpoints. |
path | string | true | ||
| payment_datetime | The datetime the payment was made, as an ISO8601 datetime (e.g. 2026-08-20T00:00:00Z). Required. |
body | string | true | ||
| payment_method_id | The id of the payment method to record this payment under. Required. Retrieve valid IDs from GET /public/v1/payment-methods. |
body | string | true | ||
| quickbooks_deposit_account_id | 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". |
body | string | false | ||
| quickbooks_deposit_account_name | 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". |
body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single payment | PaymentResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Upsert an invoice
Success scenario
POST /public/v1/invoices
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzYsImlhdCI6MTc4NzU4NzI3NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMGUwMTFiMmQtYzc1NC00NTE4LWE4NGQtYWYwYzBjZjgxNmU2IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3Mjc1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzY5NyIsInR5cCI6ImFjY2VzcyJ9.ua2iNkwvLJNmwNaCJNCqeWN5hgCivTBdRn4v7cAOQAc
{
"billing_location_id": "00000000-0000-0000-0000-00000000030e",
"charges": [
{
"name": "C1",
"percent": "10.0000",
"type": "CHARGE",
"unit_type": "PERCENT"
},
{
"name": "C2",
"price": "-5.0000",
"type": "DISCOUNT",
"unit_type": "PRICE"
}
],
"custom_data": {
"111": [
"A",
"B"
]
},
"due_datetime": "2020-01-30T00:00:01.000000Z",
"external_notes": "Visible to the customer",
"internal_notes": "Only visible internally",
"invoice_datetime": "2020-01-01T00:00:00.000000Z",
"items": [
{
"description": "Custom line note",
"order_item_id": "393dde6d-9d37-477c-a4c9-ae359fcf0bcd",
"quantity": "1.000000000"
},
{
"order_item_id": "60b45ba3-388f-4858-8600-42d27376de2e",
"quantity": "10.000000000"
}
],
"order_id": "9cb16b2b-8d67-4b7a-b7ec-d64c789ee139",
"owner_id": "00000000-0000-0000-0000-000000000e71"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 33a11161c7bc1be354360803b6cd43e0-6779922eb1a0f8d5-0
{
"data": {
"billing_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000a42",
"id": "00000000-0000-0000-0000-00000000030e",
"license_id": null,
"license_number": null,
"name": "Place 780"
},
"charges": [
{
"id": "a7f12cd3-cf64-473e-b371-bc4572a3268e",
"inserted_datetime": "2026-08-24T16:01:16.876296Z",
"name": "C1",
"percent": "10.0000",
"price": "5.30",
"type": "CHARGE",
"unit_type": "PERCENT"
},
{
"id": "26b1dc0f-9ebb-472d-9261-b436554c233f",
"inserted_datetime": "2026-08-24T16:01:16.878840Z",
"name": "C2",
"percent": null,
"price": "-5.00",
"type": "DISCOUNT",
"unit_type": "PRICE"
}
],
"company": {
"id": "00000000-0000-0000-0000-000000000589",
"name": "Company 2619",
"updated_datetime": "2026-08-24T16:01:16.729512Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "user1@a.com",
"full_name": "John Foo",
"id": "00000000-0000-0000-0000-000000000e71",
"inserted_datetime": "2026-08-24T16:01:16.751717Z",
"role": {
"id": "00000000-0000-0000-0000-000000000eaf",
"name": "Admin 3758"
}
},
"custom_data": [
{
"id": 111,
"name": "Custom Field 85",
"value": "A,B"
}
],
"due_datetime": "2020-01-30T00:00:01.000000Z",
"external_notes": "Visible to the customer",
"id": "00000000-0000-0000-0000-000000000058",
"inserted_datetime": "2026-08-24T16:01:16.874036Z",
"internal_notes": "Only visible internally",
"invoice_datetime": "2020-01-01T00:00:00.000000Z",
"invoice_number": "INV-0000001",
"items": [
{
"batch": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"description": "Custom line note",
"id": "00000000-0000-0000-0000-00000000003a",
"inserted_datetime": "2026-08-24T16:01:16.879410Z",
"order_item": {
"batch": null,
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "393dde6d-9d37-477c-a4c9-ae359fcf0bcd",
"inserted_datetime": "2026-08-24T16:01:16.831211Z",
"is_sample": false,
"leaflink_id": null,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000a42",
"id": "00000000-0000-0000-0000-00000000030e",
"license_id": null,
"name": "Place 780"
},
"note": null,
"package": null,
"price": "3.000000000",
"price_base": "3",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "b4a49af6-23a9-411f-b1d1-cee5f7b856e2",
"name": "P1",
"sku": "SKU1",
"updated_datetime": "2026-08-24T16:01:16.770526Z"
},
"quantity": "1.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
},
"order_item_id": "393dde6d-9d37-477c-a4c9-ae359fcf0bcd",
"package": null,
"price": "3.000000000",
"product": {
"id": "b4a49af6-23a9-411f-b1d1-cee5f7b856e2",
"name": "P1",
"sku": "SKU1",
"updated_datetime": "2026-08-24T16:01:16.770526Z"
},
"quantity": "1.000000000",
"returned_quantity": "0",
"total_cost_actual": null,
"total_cost_default": null
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000325",
"name": "B2"
},
"cost_per_unit": null,
"cost_per_unit_default": null,
"description": null,
"id": "00000000-0000-0000-0000-00000000003b",
"inserted_datetime": "2026-08-24T16:01:16.880620Z",
"order_item": {
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000325",
"name": "B2"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "60b45ba3-388f-4858-8600-42d27376de2e",
"inserted_datetime": "2026-08-24T16:01:16.844386Z",
"is_sample": false,
"leaflink_id": null,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000a42",
"id": "00000000-0000-0000-0000-00000000030e",
"license_id": null,
"name": "Place 780"
},
"note": null,
"package": null,
"price": "5.000000000",
"price_base": "5",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "fb7ea46a-df7b-44cd-80a6-688a7d69773f",
"name": "P2",
"sku": "SKU2",
"updated_datetime": "2026-08-24T16:01:16.789418Z"
},
"quantity": "10.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
},
"order_item_id": "60b45ba3-388f-4858-8600-42d27376de2e",
"package": null,
"price": "5.000000000",
"product": {
"id": "fb7ea46a-df7b-44cd-80a6-688a7d69773f",
"name": "P2",
"sku": "SKU2",
"updated_datetime": "2026-08-24T16:01:16.789418Z"
},
"quantity": "10.000000000",
"returned_quantity": "0",
"total_cost_actual": null,
"total_cost_default": null
}
],
"order": {
"id": "9cb16b2b-8d67-4b7a-b7ec-d64c789ee139",
"order_number": "SO-137",
"status": "PROCESSING",
"total": "0.00"
},
"owner": {
"banned": false,
"deleted_at": null,
"email": "user1@a.com",
"full_name": "John Foo",
"id": "00000000-0000-0000-0000-000000000e71",
"inserted_datetime": "2026-08-24T16:01:16.751717Z",
"role": {
"id": "00000000-0000-0000-0000-000000000eaf",
"name": "Admin 3758"
}
},
"paid_amount": "0.0",
"payment_term_name": null,
"payments": [],
"remaining_amount": "53.30",
"status": "NOT_PAID",
"total": "53.30",
"updated_datetime": "2026-08-24T16:01:16.888471Z",
"voided_datetime": null
}
}
Error scenario: owner not found
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ec6d170a46d43bac2e0b63cb21ccb660-041fcb03dc0c26d4-0
{
"errors": [
{
"context": {},
"message": "Owner does not exist",
"pointer": [
"owner_id"
],
"section": "body"
}
]
}
Error scenario: charge percent above 100
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 6c90f70c58bcc40e7192928141ab68d0-8f2755990d592c1d-0
{
"errors": [
{
"context": {
"id": "2e81221e-9218-4d31-b8d3-f10e06190dd1"
},
"message": "Must be less than or equal to 100",
"pointer": [
"charges",
0,
"percent"
],
"section": "body"
},
{
"context": {},
"message": "Order item not found",
"pointer": [
"items",
0,
"order_item_id"
],
"section": "body"
},
{
"context": {},
"message": "Only 10 left uninvoiced",
"pointer": [
"items",
1,
"quantity"
],
"section": "body"
}
]
}
Error scenario: order item on another order
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 38ccb79a9d662690eb4c1bb768e9c2a4-c17628730904ea02-0
{
"errors": [
{
"context": {},
"message": "Order item belongs to a different order",
"pointer": [
"items",
0,
"order_item_id"
],
"section": "body"
}
]
}
Create or update a single invoice. This is one endpoint for both: omit id to create a new invoice (Distru assigns the ID and the human-readable invoice number), or pass an existing invoice's id to update it. Same URL, same request shape.
Every invoice bills an existing sales order, so order_id is required and each line item must reference an order item that belongs to that order. This endpoint bills existing order lines — it does not create, reserve, consume, or otherwise move inventory. Inventory is governed by the sales order and its fulfillment, not by invoicing. On save, Distru recalculates the invoice's charge amounts and total and recomputes its payment status from recorded payments; for an update that lowers the total below what has already been paid, the excess is reconciled into a customer credit automatically.
Updates are sparse: any top-level field you omit is left unchanged, and sending an explicit null clears that field. The items and charges collections are optional on update — omit either to leave the existing line items or charges untouched. When you DO send items or charges, that array is the complete set for that collection: any existing entry whose id you leave out is deleted, and passing [] clears them all. A line item or charge sent WITH an id is patched — merged onto the stored row, so you can change one field and omit the rest; one WITHOUT an id is a new entry. If any part of the request is rejected (a bad order item, a validation failure), the whole upsert fails and nothing is changed.
A voided invoice is frozen and cannot be edited through this endpoint — the request is rejected. An invoice is voided automatically when its sales order is canceled (and un-voided if that order later leaves the canceled status), so voiding is driven by the order, not set here.
Side effects reach other systems asynchronously. A successful 200 means the invoice was saved, not that downstream syncs finished: if the company is connected to QuickBooks Online the invoice is queued for sync there, and if the invoice's customer maps to a connected point-of-sale (Treez, Dutchie, or Blaze) it is queued for sync to that POS. Poll the relevant system to observe the synced result. Distru may also email the invoice PDF to the customer when the order is configured to do so.
Required permission: invoices_permissions_create to create a new invoice, or invoices_permissions_edit (plus access to the invoice under team restrictions) to update an existing one.
Request
POST /public/v1/invoices
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| billing_location_id | 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. |
body | string | false | ||
| charges | Extra lines added on top of the items — fees, discounts, or taxes — each following the 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. |
body | array | false | ||
| custom_data | 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. | body | object | false | {"101":"Some text value","102":"2026-08-18T00:00:00.000-07:00","103":["Option A","Option B"]} | |
| due_datetime | 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. |
body | string | true | ||
| external_notes | Notes on this invoice that are visible to the customer. Optional. | body | string | false | ||
| id | 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. | body | string | false | ||
| internal_notes | Notes on this invoice that are only visible internally. Optional. | body | string | false | ||
| invoice_datetime | 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. |
body | string | true | ||
| items | The line items being billed, one entry per line, each following the 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. |
body | array | false | ||
| order_id | 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. |
body | string | true | ||
| owner_id | 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. |
body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single invoice | InvoiceResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Location
Get a location
Success scenario
GET /public/v1/locations/00000000-0000-0000-0000-000000000113
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiNmQ3YTJlMWQtYmY3Yi00ZjJjLWE1MTctZjZiZDE3ZGI4N2M5IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTM0OCIsInR5cCI6ImFjY2VzcyJ9.67o6FpzLM9nK4KckLVjtiLve88uwOEPqVqyOa_71Ipc
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c168318e6bc3450c9c704472dd3fd654-faf2a48140edb65b-0
{
"data": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"apt": null,
"city": "Beverly Hills",
"company_id": "00000000-0000-0000-0000-00000000040a",
"country": "US",
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000113",
"inserted_datetime": "2026-08-24T16:01:07.625205Z",
"latitude": 33.5,
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:07.609330Z",
"id": "00000000-0000-0000-0000-000000000030",
"inserted_datetime": "2026-08-24T16:01:07.609444Z",
"issue_datetime": "2026-08-24T16:01:07.609327Z",
"license_number": "CDPH-00000049",
"license_type": "Specialty Cottage Outdoor"
},
"license_id": "00000000-0000-0000-0000-000000000030",
"longitude": -117.2,
"metrc_id": 42,
"name": "Place 274",
"state": "CA",
"street_address": "123 Fake Street",
"updated_datetime": "2026-08-24T16:01:07.625205Z",
"zip": "90210"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 46624ea78085e672ed6701e6f74522a1-9b32d481a069a6aa-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Returns a single location by its ID, including its address fields, geo coordinates, compliance metrc_id, and its linked license. Use this to resolve a location referenced elsewhere (for example the location_id on a purchase or order) into its full detail.
Several 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.
Lookups 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.
Required permission: companies_permissions_view.
Request
GET /public/v1/locations/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single location | LocationResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get locations
Success scenario
GET /public/v1/locations
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMWZkYmE0ODgtOTkzYS00ZjkyLTk0OWQtZWIzOWU3YTMxNTEzIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDQwIiwidHlwIjoiYWNjZXNzIn0.Y-zHebL9iKOgDZEcNe7SQy2HAaJc8HVjLJdKBMqssho
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a9fdc36c092eaed12208d7a5a2d8ed09-bac8550bf694cbfd-0
{
"data": [
{
"address": "123 Fake Street, Suite 100, Beverly Hills, CA 90210, US",
"apt": "Suite 100",
"city": "Beverly Hills",
"company_id": "00000000-0000-0000-0000-000000000170",
"country": "US",
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000047",
"inserted_datetime": "2026-08-24T16:01:04.351678Z",
"latitude": 12.34,
"license": null,
"license_id": null,
"longitude": -56.78,
"metrc_id": null,
"name": "Place 70",
"state": "CA",
"street_address": "123 Fake Street",
"updated_datetime": "2026-08-24T16:01:04.351678Z",
"zip": "90210"
},
{
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"apt": null,
"city": "Beverly Hills",
"company_id": "00000000-0000-0000-0000-000000000170",
"country": "US",
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000049",
"inserted_datetime": "2026-08-24T16:01:04.368999Z",
"latitude": 1.0,
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:04.334785Z",
"id": "00000000-0000-0000-0000-000000000009",
"inserted_datetime": "2026-08-24T16:01:04.334893Z",
"issue_datetime": "2026-08-24T16:01:04.334782Z",
"license_number": "CDPH-00000010",
"license_type": "Type N Infusions"
},
"license_id": "00000000-0000-0000-0000-000000000009",
"longitude": 2.0,
"metrc_id": 999,
"name": "Place 72",
"state": "CA",
"street_address": "123 Fake Street",
"updated_datetime": "2026-08-24T16:01:04.368999Z",
"zip": "90210"
}
],
"next_page": null
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 83d40e0780757b8c1a3f6e87e162a2e4-7c3677690b5e3aaa-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"updated_datetime"
],
"section": "query"
}
]
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 83d40e0780757b8c1a3f6e87e162a2e4-8d24c3c8655f08aa-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"updated_datetime"
],
"section": "query"
}
]
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 83d40e0780757b8c1a3f6e87e162a2e4-0c29d83f9b7b1008-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"updated_datetime"
],
"section": "query"
}
]
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 83d40e0780757b8c1a3f6e87e162a2e4-9f96e2d98705d415-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"updated_datetime"
],
"section": "query"
}
]
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 83d40e0780757b8c1a3f6e87e162a2e4-0c9009f67839a350-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"updated_datetime"
],
"section": "query"
}
]
}
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 receives inventory, where an order ships from, and where stock and packages are held. Each location optionally links to a compliance 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.
By 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.
Results 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.
This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.
Required permission: companies_permissions_view.
Request
GET /public/v1/locations
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| deleted | 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.no include only |
query | string | false | no | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z, | |
| license_number | 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. |
query | string | false | ?license_number=C11-0000001-LIC | |
| name | Case-insensitive substring match on the location name (partial matches count; e.g. main matches Main Warehouse). Send a single value, not a list. |
query | string | false | ?name=warehouse | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of locations | Locations |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Menu
Get a menu
Success scenario
GET /public/v1/menus/00000000-0000-0000-0000-000000000039
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjgsImlhdCI6MTc4NzU4NzI2OCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYjJkZWJmNmEtNmJjYi00NWVlLTg3ZDQtYzNmZjViNDNmZWM3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY3LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTc1OCIsInR5cCI6ImFjY2VzcyJ9.NJYaWVdHPU94U001NDoGl8okpjyKZlKEsEuGVBR3GIs
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: d60c141f094a4c09c17c199bbde9c8dd-9991c7fc59157f2a-0
{
"data": {
"active": true,
"available_delivery_days": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
"SUNDAY"
],
"default_order_status": "PENDING",
"discoverable": true,
"external_name": "External Test Menu",
"id": "00000000-0000-0000-0000-000000000039",
"inserted_datetime": "2026-08-24T16:01:08.865406Z",
"internal_name": "Test Menu",
"minimum_order_lead_time_days": 0,
"minimum_order_subtotal": "50.5",
"product_count": 1,
"updated_datetime": "2026-08-24T16:01:08.865406Z",
"url": "https://distru.com/menu/company/test",
"visibility": "PUBLIC"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 6c643a253cdc617decaefeda031fd5b4-06ff81d9d1f6429b-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Required permission: products_permissions_view (admins are always allowed).
Request
GET /public/v1/menus/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The menu's ID, as returned in the id field of the list and show responses. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single menu | MenuResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get menus
Success scenario
GET /public/v1/menus
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiODJhMTE5NWYtODM1MC00ZWU5LWEwMDAtNGZjMDhhNWFhZDQxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDYyIiwidHlwIjoiYWNjZXNzIn0.zCMoLwHyE78xk9lRGq3ElfQ9OUYf9IMGklhWoPygoZs
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 20cf92f89200d9e32fbb73f95c0ff2a5-9680689c2fc89f7b-0
{
"data": [
{
"active": true,
"available_delivery_days": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
"SUNDAY"
],
"default_order_status": "PENDING",
"discoverable": true,
"external_name": "Ext A",
"id": "00000000-0000-0000-0000-000000000002",
"inserted_datetime": "2026-08-24T16:01:04.456882Z",
"internal_name": "Alpha",
"minimum_order_lead_time_days": 0,
"minimum_order_subtotal": null,
"product_count": 0,
"updated_datetime": "2026-08-24T16:01:04.456882Z",
"url": null,
"visibility": "PUBLIC"
},
{
"active": true,
"available_delivery_days": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
"SUNDAY"
],
"default_order_status": "PENDING",
"discoverable": true,
"external_name": "Ext B",
"id": "00000000-0000-0000-0000-000000000003",
"inserted_datetime": "2026-08-24T16:01:04.475385Z",
"internal_name": "Beta",
"minimum_order_lead_time_days": 0,
"minimum_order_subtotal": null,
"product_count": 0,
"updated_datetime": "2026-08-24T16:01:04.475385Z",
"url": null,
"visibility": "PUBLIC"
}
],
"next_page": null
}
Error scenario: invalid active filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b52b2b50e5a79ca947cf516e704c7e97-c03d86d22494cf69-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"active"
],
"section": "query"
}
]
}
List menus for the authenticated company, ordered oldest-first by creation time.
A 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).
Results 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).
Required permission: products_permissions_view.
Request
GET /public/v1/menus
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| active | 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. |
query | boolean | false | ?active=true | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-01T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=,2022-07-10T00:00:00Z | |
| visibilities | 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.PUBLIC PRIVATE PASSCODE_PROTECTED |
query | array | false | ?visibilities[]=PUBLIC&visibilities[]=PASSCODE_PROTECTED | |
| visibility | 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. |
query | string | false | ?visibility=PUBLIC,PASSCODE_PROTECTED |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Menus index | Menus |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Metrc
Get Metrc items
Success scenario
GET /public/v1/metrc/items
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiNDdlZjc0ZDgtOGMzZi00MDdiLWEwMDUtYzRlODA5NDQyZjE0IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTE5NyIsInR5cCI6ImFjY2VzcyJ9.EWIldFmtAoSKE72xicv7KCNmpvwd9ycvMDsgg2hGR4k
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: bf00f33e242ce6c3cac7b98104f05820-910235409ef57a77-0
{
"data": [
{
"inserted_datetime": "2026-08-24T16:01:07.092133Z",
"is_deleted": false,
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:07.088878Z",
"id": "00000000-0000-0000-0000-000000000026",
"inserted_datetime": "2026-08-24T16:01:07.088958Z",
"issue_datetime": "2026-08-24T16:01:07.088876Z",
"license_number": "CDPH-00000039",
"license_type": "Medium Mixed-Light Tier 2"
},
"metrc_id": 1000006,
"metrc_inserted_datetime": "2026-08-19T12:11:27.46Z",
"metrc_strain_id": 35,
"metrc_unit_name": "Ounces",
"name": "Blue Dream 3.5g",
"product_category_name": "Buds",
"product_category_type": "Buds",
"quantity_type": "WEIGHT_BASED",
"strain_name": "Blue Dream",
"unit_type": {
"id": "00000000-0000-0000-0000-000000002d04",
"name": "Ounce"
},
"updated_datetime": "2026-08-24T16:01:07.092133Z"
}
],
"next_page": null
}
Error scenario: invalid unit_type_category filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ac9798657d7886462832eebe9168e5a9-7fa002e2cfd6b620-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"unit_type_category"
],
"section": "query"
}
]
}
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.
Each 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.
This 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.
Request
GET /public/v1/metrc/items
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| category_names | 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). |
query | array | false | ?category_names[]=Buds&category_names[]=Pre-Roll | |
| inserted_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z, | |
| is_deleted | Filter by whether the item is deleted in Metrc. true returns only deleted items; false returns only live items. Omit to return both. |
query | boolean | false | false | |
| license_id | 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. | query | string | false | b1f4c2a0-9c3e-4d2b-8f1a-2e5c6d7a8b9c | |
| metrc_ids | 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). |
query | array | false | ?metrc_ids[]=84213&metrc_ids[]=84214 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| search | Filter to items whose Metrc name contains this value (case-insensitive substring match against the item's name). |
query | string | false | blue dream | |
| unit_type_category | 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.COUNT VOLUME WEIGHT |
query | string | false | WEIGHT | |
| updated_datetime | 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. |
query | string | false | ,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of Metrc items | MetrcItems |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get Metrc tags
Success scenario
GET /public/v1/metrc/tags
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMTdlNTBjMjktMWRiZi00NGI4LTgyZGItZTYyNDQ2ODk4MTRiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODYyIiwidHlwIjoiYWNjZXNzIn0.COU7QHGYFrIcDQXl8fo-qiFaTAsFXhEZDIQvDow01hg
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 693cd088a6bbad0777f7b6153dcfb54c-43d38e3b5f0f61cd-0
{
"data": [
{
"assigned_datetime": "2024-01-02T12:00:00.000000Z",
"commissioned_date": "2026-08-24",
"id": "00000000-0000-0000-0000-000000000007",
"inserted_datetime": "2026-08-24T16:01:05.985376Z",
"is_assigned": true,
"kind": "PACKAGE",
"license_id": "00000000-0000-0000-0000-000000000015",
"tag": "1A4010200001234000000001",
"updated_datetime": "2026-08-24T16:01:05.985376Z"
}
],
"next_page": null
}
Error scenario: invalid kind filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 85042d36fe33402529c255c921da26ce-3b4ca60e0b264008-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"kind"
],
"section": "query"
}
]
}
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.
This 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.
Data is eventually consistent — a tag newly synced from Metrc can take up to ~1 second to appear here.
Request
GET /public/v1/metrc/tags
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z, | |
| is_assigned | 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. |
query | boolean | false | false | |
| kind | Filter by tag kind. PACKAGE covers retail/wholesale package labels; PLANT covers plant tags. Omit to return both kinds.PACKAGE PLANT |
query | string | false | PACKAGE | |
| license_id | 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. | query | string | false | b1f4c2a0-9c3e-4d2b-8f1a-2e5c6d7a8b9c | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| search | 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. |
query | string | false | 0004999 | |
| tag | 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. |
query | string | false | 1A4FF0100000022000004999 | |
| updated_datetime | 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. |
query | string | false | ,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of Metrc tags | MetrcTags |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get a Metrc tag
Success scenario
GET /public/v1/metrc/tags/00000000-0000-0000-0000-00000000000a
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTgwY2E0NDktNDNlNC00YmJmLWEyOGItOTdjYmY1MDFiYzRhIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6OTM1IiwidHlwIjoiYWNjZXNzIn0.pehfUs61SUBg7lC0tqxqebWbZV_7-bI0mpC-kiuUzsc
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 8c27e32d1c7d8afae0415de49267c160-876414a283055642-0
{
"data": {
"assigned_datetime": null,
"commissioned_date": "2026-08-24",
"id": "00000000-0000-0000-0000-00000000000a",
"inserted_datetime": "2026-08-24T16:01:06.223758Z",
"is_assigned": false,
"kind": "PACKAGE",
"license_id": "00000000-0000-0000-0000-000000000019",
"tag": "1A4010200001234000000001",
"updated_datetime": "2026-08-24T16:01:06.223758Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: eca23f8c7ec9f8ee828fef1d713c579b-783bdc2af0e614fe-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Request
GET /public/v1/metrc/tags/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The Distru ID of the tag, as returned in the id field of the tags list (not the Metrc tag label). |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single Metrc tag | MetrcTagResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
OfficialProductCategory
Get official product categories
Success scenario
GET /public/v1/official-product-categories
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjEsImlhdCI6MTc4NzU4NzI2MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiM2ZmYjcxNjQtZTc3Ni00OWMwLTgzNDEtOWMzYWRhYzZhNWVhIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MyIsInR5cCI6ImFjY2VzcyJ9.lrvmIZHbX9of-pgQXg558mcHqXzOABEJ7KJ8MM5KbzI
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 3d49d47be2a212bef04b9f02470c33ce-9fd26f0a8f30ed0e-0
{
"data": [
{
"id": "CAPSULES",
"name": "Capsules"
},
{
"id": "CLONES",
"name": "Clones & Seeds"
},
{
"id": "CONCENTRATES",
"name": "Concentrates"
},
{
"id": "EDIBLES",
"name": "Edibles"
},
{
"id": "FLOWER",
"name": "Flower"
},
{
"id": "MERCH",
"name": "Merch"
},
{
"id": "OTHER",
"name": "Other"
},
{
"id": "PREROLLS",
"name": "Pre-Rolls"
},
{
"id": "TINCTURES",
"name": "Tinctures"
},
{
"id": "TOPICALS",
"name": "Topicals"
},
{
"id": "VAPES",
"name": "Vapes"
}
]
}
List Distru's official product categories — the global, system-defined reference
taxonomy (e.g. FLOWER, PRE_ROLL, EDIBLE) that is identical for every company and
is not scoped to your API key. These records are singletons maintained by Distru; they
cannot be created, edited, or deleted through the API.
Use this to resolve the official_product_category_id on your own
product categories: each of your product categories maps to
exactly one of these, and that mapping is what lets Distru report across accounts, drive
consistent menu/marketplace filtering, and let buyers configure POS integrations without
knowing your custom category names. Fetch this list to discover the valid category id
values before you set that mapping; you do not need it for anything else.
The full set is returned in a single call ordered by id ascending — there is no
filtering, no pagination, and no next_page. The list is small and stable, so it is
safe to cache; new entries are added only when Distru extends the taxonomy.
Request
GET /public/v1/official-product-categories
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of official product categories | OfficialProductCategories |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Order
Delete an order
Success scenario
DELETE /public/v1/orders/18a6cda2-3440-4440-ac2d-842b0ffb06fb
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzMsImlhdCI6MTc4NzU4NzI3MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmE0NTNlYjgtNjRkZS00NTNlLWI0NGEtOGE3ZWYxYTBjYzk1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzE0MiIsInR5cCI6ImFjY2VzcyJ9.prQ9oJP7X1X_OLC9F0AlSUp3TwgJHRCOSQn0mm-JP5o
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 8c2f5a1d94e3ba702811d6c7d10286db-4b7edcd33594b7aa-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cf914631d8868f824545ba38e52cf1ab-aa618aa77fb9cfbe-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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).
Some orders cannot be deleted; each of these is refused with a 400 and nothing is changed:
• 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. • 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. • An order with returns recorded against it. • A historical shared-license order. • Any custom validation rule your company has configured for order deletion can also refuse the delete.
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.
The 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).
Other 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.
Required permission: orders_permissions_delete (plus access to the order under team restrictions).
Request
DELETE /public/v1/orders/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get an order
Success scenario
GET /public/v1/orders/18a6cda2-3440-4440-ac2d-842b0ffb06fb
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4ODUsImlhdCI6MTc4NzU4NzI4NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTNkZWViZTAtZGE1YS00ZTU4LWI2MzctNjAzYTVmZjEzYTgxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3Mjg0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDg4MyIsInR5cCI6ImFjY2VzcyJ9.W-1GvcpwTxntz8YtrOPAmww2twccQvYJ893Tc0Ezq-g
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 045997f2fe2cf41484749e11f177734a-764b0774f2be3fa6-0
{
"data": {
"billing_location": null,
"biotrack_id": null,
"blaze_payment_type": null,
"buyer_company": null,
"buyer_note": null,
"charges": [
{
"id": "bbeb7a6b-e9fc-4e8d-8830-ee400b9c9ab2",
"inserted_datetime": "2026-08-24T16:01:25.466659Z",
"name": "C1",
"percent": "10.0000",
"price": "1.00",
"tax": {
"id": "00000000-0000-0000-0000-000000000017",
"name": "T1"
},
"type": "CHARGE",
"unit_type": "PERCENT"
}
],
"combined_order": null,
"company": {
"id": "00000000-0000-0000-0000-000000000879",
"name": "Company 3554",
"updated_datetime": "2026-08-24T16:01:25.388867Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-4846@example.com",
"full_name": "FirstName9802 LastName9803",
"id": "00000000-0000-0000-0000-000000001314",
"inserted_datetime": "2026-08-24T16:01:25.384556Z",
"role": {
"id": "00000000-0000-0000-0000-000000001328",
"name": "Admin 4903"
}
},
"custom_data": [
{
"id": 119,
"name": "Custom Field 93",
"value": "Custom Field Value 1"
}
],
"delivered_datetime": "2026-08-24T16:01:25.403490Z",
"delivery_datetime": null,
"due_datetime": "2026-08-24T16:01:25.403507Z",
"external_notes": null,
"id": "18a6cda2-3440-4440-ac2d-842b0ffb06fb",
"inserted_datetime": "2026-08-24T16:01:25.403836Z",
"internal_notes": null,
"inventory_source": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000de6",
"id": "00000000-0000-0000-0000-000000000497",
"license_id": null,
"license_number": null,
"name": "Place 1173"
},
"invoices": [],
"items": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000550",
"name": "B4221"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "7a45b2c9-9a25-4089-bf53-5b692dd5e614",
"inserted_datetime": "2026-08-24T16:01:25.414321Z",
"is_sample": false,
"leaflink_id": null,
"location": null,
"note": null,
"package": null,
"price": "10.000000000",
"price_base": "10",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "c15f85f5-00f4-4640-92c4-ed902c9a2423",
"name": "Product 4219",
"sku": "sku 4220",
"updated_datetime": "2026-08-24T16:01:25.411943Z"
},
"quantity": "15.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000551",
"name": "B4224"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "8a781565-1db5-4c51-b99b-bab6457bb0c3",
"inserted_datetime": "2026-08-24T16:01:25.425018Z",
"is_sample": false,
"leaflink_id": null,
"location": null,
"note": null,
"package": null,
"price": "10.000000000",
"price_base": "10",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "5f83e1bb-94b9-4c5a-82c8-e3be3d3ab4e0",
"name": "Product 4222",
"sku": "sku 4223",
"updated_datetime": "2026-08-24T16:01:25.422971Z"
},
"quantity": "10.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000552",
"name": "B4227"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "feede1c4-508c-4f1c-b4ab-5776ad424192",
"inserted_datetime": "2026-08-24T16:01:25.434522Z",
"is_sample": false,
"leaflink_id": null,
"location": null,
"note": null,
"package": null,
"price": "10.000000000",
"price_base": "10",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "809d0d8f-a461-4f9c-8da6-11b09a2a49bd",
"name": "Product 4225",
"sku": "sku 4226",
"updated_datetime": "2026-08-24T16:01:25.432519Z"
},
"quantity": "5.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000553",
"name": "B4230"
},
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "edeef081-6dc2-47ce-82ae-8da15bdae3b0",
"inserted_datetime": "2026-08-24T16:01:25.444371Z",
"is_sample": false,
"leaflink_id": null,
"location": null,
"note": null,
"package": null,
"price": "10.000000000",
"price_base": "10",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "aba133dd-a898-48b7-bc4e-7d3fa9326f74",
"name": "Product 4228",
"sku": "sku 4229",
"updated_datetime": "2026-08-24T16:01:25.441972Z"
},
"quantity": "2.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
}
],
"leaflink_id": null,
"leaflink_order_number": null,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000de6",
"id": "00000000-0000-0000-0000-000000000497",
"license_id": null,
"license_number": null,
"name": "Place 1173"
},
"menu": null,
"metrc_transfer_id": null,
"metrc_transfer_template_error": null,
"metrc_transfer_template_id": null,
"metrc_transfer_template_status": null,
"order_datetime": "2026-08-24T16:01:25.403506Z",
"order_number": "SO-240",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-4846@example.com",
"full_name": "FirstName9802 LastName9803",
"id": "00000000-0000-0000-0000-000000001314",
"inserted_datetime": "2026-08-24T16:01:25.384556Z",
"role": {
"id": "00000000-0000-0000-0000-000000001328",
"name": "Admin 4903"
}
},
"payment_term_name": null,
"returns": [],
"shipping_location": null,
"status": "COMPLETED",
"total": "320.00",
"updated_datetime": "2026-08-24T16:01:25.458865Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 2107dcf7dd8677c489d0b5081e1db708-6c939cf114d42f25-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get a single 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.
Note: 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.
Required 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.
Request
GET /public/v1/orders/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | Order ID | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single order | OrderResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get orders
Success scenario
GET /public/v1/orders
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjksImlhdCI6MTc4NzU4NzI2OSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZDVhMjJlMTAtMDNhNC00MGFhLThhZTEtZjAyMjEzZTE1YjQxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY4LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTg2MyIsInR5cCI6ImFjY2VzcyJ9.3TY5EPjZYMDYiA1Cp56xe9ty6nbJUy-HxL62pst8eQU
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 7ad1802d1cf43c88b6447075d1aaddf0-24b40df8899b5f46-0
{
"data": [
{
"billing_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000537",
"id": "00000000-0000-0000-0000-00000000016b",
"license_id": null,
"license_number": null,
"name": "Place 362"
},
"biotrack_id": null,
"blaze_payment_type": "CASH",
"buyer_company": null,
"buyer_note": null,
"charges": [
{
"id": "0c1bb83f-42f8-4d83-bb59-0eb02749e5dc",
"inserted_datetime": "2026-08-24T16:01:09.315387Z",
"name": "C1",
"percent": "10.0000",
"price": "1.00",
"tax": {
"id": "00000000-0000-0000-0000-000000000010",
"name": "T1"
},
"type": "CHARGE",
"unit_type": "PERCENT"
}
],
"combined_order": null,
"company": {
"id": "00000000-0000-0000-0000-000000000247",
"name": "Company 1387",
"updated_datetime": "2030-11-01T00:00:00.000000Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "user1@a.com",
"full_name": "John Foo",
"id": "00000000-0000-0000-0000-000000000729",
"inserted_datetime": "2026-08-24T16:01:09.035306Z",
"role": {
"id": "00000000-0000-0000-0000-00000000074e",
"name": "Admin 1869"
}
},
"custom_data": [
{
"id": 68,
"name": "Custom Field 46",
"value": "Custom Field Value 1"
}
],
"delivered_datetime": "2020-01-03T00:00:00.000000Z",
"delivery_datetime": "2020-01-01T00:00:00.000000Z",
"due_datetime": "2020-01-01T00:00:01.000000Z",
"external_notes": null,
"id": "7e7d2641-801e-4168-9f30-78c9538a987c",
"inserted_datetime": "2020-01-01T00:00:03.000000Z",
"internal_notes": "Internal notes for this order",
"inventory_source": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000537",
"id": "00000000-0000-0000-0000-00000000016b",
"license_id": null,
"license_number": null,
"name": "Place 362"
},
"invoices": [],
"items": [
{
"batch": null,
"compliance_quantity": "10.0000",
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "fbeb5c03-ea40-4b93-a6d3-276920cc4234",
"inserted_datetime": "2026-08-24T16:01:09.299324Z",
"is_sample": true,
"leaflink_id": null,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000537",
"id": "00000000-0000-0000-0000-00000000016b",
"license_id": null,
"name": "Place 362"
},
"note": null,
"package": {
"batch_number": "B1",
"compliance_label": "ABCDEF012345670000000088",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-00000000002f",
"license_id": "00000000-0000-0000-0000-000000000048",
"location_id": "00000000-0000-0000-0000-000000000173",
"metrc_id": 87,
"metrc_label": "ABCDEF012345670000000088",
"quantity": "10.000000000",
"quantity_active": "10.000000000",
"status": "active"
},
"price": "10.000000000",
"price_base": "10",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "75428da1-f766-4d00-b600-aa1662926802",
"name": "P1",
"sku": "SKU1",
"updated_datetime": "2023-11-02T00:00:00.000000Z"
},
"quantity": "1.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
}
],
"leaflink_id": "1",
"leaflink_order_number": "385edbce-543e-4862-b5aa-08f5750c201e",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000537",
"id": "00000000-0000-0000-0000-00000000016b",
"license_id": null,
"license_number": null,
"name": "Place 362"
},
"menu": null,
"metrc_transfer_id": 1,
"metrc_transfer_template_error": null,
"metrc_transfer_template_id": 2,
"metrc_transfer_template_status": "COMPLETED",
"order_datetime": "2020-01-01T00:00:02.000000Z",
"order_number": "SO-123",
"owner": {
"banned": false,
"deleted_at": null,
"email": "user2@a.com",
"full_name": "John Bar",
"id": "00000000-0000-0000-0000-000000000743",
"inserted_datetime": "2026-08-24T16:01:09.132486Z",
"role": {
"id": "00000000-0000-0000-0000-000000000767",
"name": "Admin 1894"
}
},
"payment_term_name": null,
"returns": [],
"shipping_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000537",
"id": "00000000-0000-0000-0000-00000000016b",
"license_id": null,
"license_number": null,
"name": "Place 362"
},
"status": "COMPLETED",
"total": "11.00",
"updated_datetime": "2020-01-01T00:00:04.000000Z"
}
],
"next_page": null
}
Error scenario: invalid status filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 363a5612b68f33a5e4796638de2cbc89-29fb625c09cfda5d-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"statuses"
],
"section": "query"
}
]
}
List sales orders, most recent order date first, filtered by the query parameters below. Each entry is the same full order object returned by GET /public/v1/orders/{id}, including its line items, charges, invoices, and returns.
Results 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).
Required 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.
Request
GET /public/v1/orders
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_batch_numbers | 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). | query | array | false | ?batch_batch_numbers[]=B-1001 | |
| batch_ids | 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). | query | array | false | ?batch_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| billing_location_ids | 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. |
query | array | false | ?billing_location_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| biotrack_ids | 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. |
query | array | false | ?biotrack_ids[]=0000000123 | |
| buyer_company_ids | 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. |
query | array | false | ?buyer_company_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| company_group_ids | 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. | query | array | false | ?company_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| company_ids | 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. |
query | array | false | ?company_ids[]=550e8400-e29b-41d4-a716-446655440000&company_ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| custom_data | 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. |
query | object | false | ?custom_data[101]=Blue&custom_data[102]=Wholesale | |
| delivered_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z, | |
| delivery_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z, | |
| due_datetime | 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). |
query | string | false | ,2022-07-10T00:00:00Z | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | Filter by when the order was created in Distru (its inserted_datetime). Inclusive ISO8601 range after,before; either side may be omitted. |
query | string | false | 2022-07-10T00:00:00Z, | |
| leaflink_ids | 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. |
query | array | false | ?leaflink_ids[]=123456 | |
| location_ids | 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. |
query | array | false | ?location_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| menu_ids | 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. |
query | array | false | ?menu_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| metrc_transfer_ids | 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. |
query | array | false | ?metrc_transfer_ids[]=987654 | |
| order_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z,2022-07-11T00:00:00Z | |
| order_number | 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. |
query | string | false | ?order_number=SO-10 | |
| order_numbers | 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. |
query | array | false | ?order_numbers[]=SO-1001&order_numbers[]=SO-1002 | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| package_batch_numbers | 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). | query | array | false | ?package_batch_numbers[]=B-1001 | |
| package_compliance_labels | 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). | query | array | false | ?package_compliance_labels[]=1A4FF0100000022000000123 | |
| package_ids | 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). | query | array | false | ?package_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| payment_statuses | 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:
NOT_PAID PARTIALLY_PAID FULLY_PAID OVER_PAID |
query | array | false | ?payment_statuses[]=NOT_PAID&payment_statuses[]=PARTIALLY_PAID | |
| product_brand_ids | 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). | query | array | false | ?product_brand_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_category_ids | 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). | query | array | false | ?product_category_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_group_ids | 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). | query | array | false | ?product_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_ids | 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). | query | array | false | ?product_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_skus | 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). | query | array | false | ?product_skus[]=SKU-1001&product_skus[]=SKU-1002 | |
| product_strain_ids | 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). | query | array | false | ?product_strain_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_subcategory_ids | 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). | query | array | false | ?product_subcategory_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_tag_ids | 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). | query | array | false | ?product_tag_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_vendor_ids | 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). | query | array | false | ?product_vendor_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| shipping_location_ids | 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. |
query | array | false | ?shipping_location_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| statuses | 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 for what each value means. At most 200 statuses may be given.PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
query | array | false | ?statuses[]=PENDING&statuses[]=PROCESSING | |
| total | 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. |
query | string | false | 100,500 | |
| updated_datetime | 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. |
query | string | false | ,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of orders | Orders |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Upsert an order
Success scenario
POST /public/v1/orders
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4OTUsImlhdCI6MTc4NzU4NzI5NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiODY4NTcyZDAtM2RjMi00MTJjLTlmZjctNjEwZWMzMDRhMmJkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3Mjk0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NTEyMyIsInR5cCI6ImFjY2VzcyJ9.b8ISfAfaYgbomOHYAvUmBjKV6B4s2UbG5BpzND-mKAQ
{
"billing_location_id": "00000000-0000-0000-0000-0000000004e9",
"charges": [
{
"name": "C1",
"percent": "10.0000",
"type": "CHARGE",
"unit_type": "PERCENT"
},
{
"name": "C2",
"price": "-5.0000",
"type": "DISCOUNT",
"unit_type": "PRICE"
}
],
"company_id": "00000000-0000-0000-0000-000000000921",
"delivery_datetime": "2020-01-01T00:00:00.000000Z",
"due_datetime": "2020-01-01T00:00:01.000000Z",
"external_notes": "Thank you for ordering!",
"internal_notes": "Internal notes for this order",
"items": [
{
"location_id": "00000000-0000-0000-0000-0000000004e8",
"price_base": "10.000000000",
"product_id": "29aa99b0-36f0-46a1-ab81-3e03a2f54abf",
"quantity": "1.000000000"
}
],
"order_datetime": "2020-01-01T00:00:02.000000Z",
"owner_id": "00000000-0000-0000-0000-000000001403",
"shipping_location_id": "00000000-0000-0000-0000-0000000004e9",
"status": "PROCESSING"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4530e1ca97bdf4c255a9930ee5f825c4-b516ad444aeb568f-0
{
"data": {
"billing_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000eb6",
"id": "00000000-0000-0000-0000-0000000004e9",
"license_id": null,
"license_number": null,
"name": "Place 1255"
},
"biotrack_id": null,
"blaze_payment_type": null,
"buyer_company": null,
"buyer_note": null,
"charges": [
{
"id": "c7e621ac-e800-465c-8263-cb8cc1d791b7",
"inserted_datetime": "2026-08-24T16:01:35.556895Z",
"name": "C1",
"percent": "10.0000",
"price": "1.00",
"type": "CHARGE",
"unit_type": "PERCENT"
},
{
"id": "1a8ecc90-69e4-4697-b2c3-665d6034e786",
"inserted_datetime": "2026-08-24T16:01:35.560476Z",
"name": "C2",
"percent": null,
"price": "-5.00",
"type": "DISCOUNT",
"unit_type": "PRICE"
}
],
"combined_order": null,
"company": {
"id": "00000000-0000-0000-0000-000000000921",
"name": "Company 3759",
"updated_datetime": "2026-08-24T16:01:35.394420Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "user1@a.com",
"full_name": "John Foo",
"id": "00000000-0000-0000-0000-000000001403",
"inserted_datetime": "2026-08-24T16:01:35.427238Z",
"role": {
"id": "00000000-0000-0000-0000-00000000141f",
"name": "Admin 5150"
}
},
"custom_data": [
{
"id": 122,
"name": "Custom Field 96",
"value": null
}
],
"delivered_datetime": null,
"delivery_datetime": "2020-01-01T00:00:00.000000Z",
"due_datetime": "2020-01-01T00:00:01.000000Z",
"external_notes": "Thank you for ordering!",
"id": "c4247cbb-5194-41ca-a799-95c4ff12e888",
"inserted_datetime": "2026-08-24T16:01:35.553668Z",
"internal_notes": "Internal notes for this order",
"inventory_source": null,
"invoices": [],
"items": [
{
"batch": null,
"compliance_quantity": null,
"cost_per_unit": null,
"cost_per_unit_default": null,
"id": "39b6a366-497c-44b8-b0e6-ffa14d01ebda",
"inserted_datetime": "2026-08-24T16:01:35.561589Z",
"is_sample": false,
"leaflink_id": null,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000eb5",
"id": "00000000-0000-0000-0000-0000000004e8",
"license_id": "00000000-0000-0000-0000-00000000014f",
"name": "Place 1254"
},
"note": null,
"package": null,
"price": "10.000000000",
"price_base": "10.000000000",
"price_tier_mode": "AUTO",
"price_tier_version": null,
"product": {
"id": "29aa99b0-36f0-46a1-ab81-3e03a2f54abf",
"name": "P1",
"sku": "SKU1",
"updated_datetime": "2026-08-24T16:01:35.457677Z"
},
"quantity": "1.000000000",
"returned_quantity": "0",
"thc_percentage_total": null,
"total_cost_actual": null,
"total_cost_default": null
}
],
"leaflink_id": null,
"leaflink_order_number": null,
"location": null,
"menu": null,
"metrc_transfer_id": null,
"metrc_transfer_template_error": null,
"metrc_transfer_template_id": null,
"metrc_transfer_template_status": null,
"order_datetime": "2020-01-01T00:00:02.000000Z",
"order_number": "SO-0000001",
"owner": {
"banned": false,
"deleted_at": null,
"email": "user1@a.com",
"full_name": "John Foo",
"id": "00000000-0000-0000-0000-000000001403",
"inserted_datetime": "2026-08-24T16:01:35.427238Z",
"role": {
"id": "00000000-0000-0000-0000-00000000141f",
"name": "Admin 5150"
}
},
"payment_term_name": null,
"returns": [],
"shipping_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000eb6",
"id": "00000000-0000-0000-0000-0000000004e9",
"license_id": null,
"license_number": null,
"name": "Place 1255"
},
"status": "PROCESSING",
"total": "6.00",
"updated_datetime": "2026-08-24T16:01:35.682462Z"
}
}
Error scenario: owner not found
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 8a470abda0ef17fcd31ac9e289f53cd8-b693a2f11cd1994d-0
{
"errors": [
{
"context": {
"id": "6e5f71d9-a1a4-42c3-8a8b-1d049cf391b2"
},
"message": "Owner does not exist",
"pointer": [
"owner_id"
],
"section": "body"
}
]
}
Error scenario: charge percent above 100
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 683bf76b097e385eb16fa3ad2459e188-d1546f5d71fbb5f9-0
{
"errors": [
{
"context": {
"id": "161ed879-a6e9-4e1e-89b2-c1b1d463fcef"
},
"message": "Must be less than or equal to 100",
"pointer": [
"charges",
0,
"percent"
],
"section": "body"
}
]
}
Error scenario: insufficient item inventory
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 3f661a832ec860c9b579c0535bf91c88-395be13b81e5eb59-0
{
"errors": [
{
"context": {
"id": "2abb144b-f21d-44ff-86a0-a3c675196f94"
},
"message": "There is only 0 g of P1 available at location L1.",
"pointer": [
"items",
0,
"quantity"
],
"section": "body"
},
{
"context": {
"id": "9ec5fa25-c016-40fe-84e5-65ee93ac7e01"
},
"message": "There is only 0 g of P2 available at location L1.",
"pointer": [
"items",
1,
"quantity"
],
"section": "body"
}
]
}
Create or update a single sales order. Omit id to create a new order (Distru assigns the order number and id); pass an existing order's id to update it. An id that doesn't exist, or belongs to another company, returns a not-found error.
Updates are sparse at the top level: send only the fields you want to change — every field you omit (including status) keeps its current value. The items and charges collections work differently. Omit the whole items (or charges) field and its existing rows are left untouched. Send the field and it fully replaces that set: an existing row whose id you omit is deleted, an entry whose id matches an existing row updates it (omitted fields on that entry are kept from the existing row, so you can patch a single field by sending just its id and the change), and an entry whose id is new (or omitted — Distru then assigns one) is added. Sending items as an empty array removes every line, which an order cannot be left in, so it is rejected. The whole upsert is atomic: if any part is rejected (a validation error, a disallowed status transition, an unfulfillable line) nothing is changed and the response is a 400 whose errors point at the offending field.
Setting status moves the order through its lifecycle and drives inventory. A PENDING or CANCELED order reserves nothing and touches no inventory; moving to PROCESSING commits sellable inventory — assigning a package or batch to a line item moves that quantity into a committed selling state and any unfulfilled line adds to the product's reserved quantity; READY_TO_SHIP, DELIVERING, DELIVERED, and COMPLETED additionally require every line item to be fulfilled. See the status field on the order for the full per-status behavior and transition requirements.
Compliance: associating the order with a state transfer is one-system-only — send metrc_transfer_id OR biotrack_id, never both. Doing so builds the order from that outgoing Metrc/BioTrack transfer. Any order carrying package-tracked items must be associated with a compliance transfer before it can reach DELIVERING, DELIVERED, or COMPLETED.
Invoicing: set upsert_invoice to create or refresh this order's invoice, and email_invoice to email it. The response returns the saved order with its recomputed total, line items (with cost fields), charges, invoices, and returns.
Required permission: orders_permissions_create to create, orders_permissions_edit (plus access to the order under team restrictions) to update.
Request
POST /public/v1/orders
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| billing_location_id | The billing location's ID (a Distru location ID). Optional. | body | string | false | ||
| biotrack_id | 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. |
body | string | false | ||
| blaze_payment_type | 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. CASH CREDIT DEBIT COD ACH_TRANSFER CHEQUE OTHER |
body | string | false | CASH | |
| charges | 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 shape. |
body | array | false | ||
| company_id | 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. |
body | string | false | ||
| custom_data | 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. | body | object | false | {"101":"Leave at the loading dock","102":"2026-08-18T00:00:00.000-07:00","103":["Fragile","Signature Required"]} | |
| delivery_datetime | ISO8601 datetime the order was / will be delivered. Optional; null when the order has no delivery datetime set. | body | string | false | 2022-07-10T00:00:00Z | |
| due_datetime | 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). | body | string | false | ||
| email_invoice | 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. |
body | boolean | false | ||
| email_invoice_addresses | 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. |
body | string | false | amy@distru.com,john@distru.com | |
| external_notes | 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. | body | string | false | ||
| id | 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. | body | string | false | ||
| internal_notes | Free-form notes visible only inside Distru; never shown to the customer. Use external_notes for a customer-facing message. |
body | string | false | ||
| items | 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 shape. |
body | array | false | ||
| location_id | 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. |
body | string | false | ||
| metrc_transfer_id | 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. |
body | integer | false | ||
| order_datetime | 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. | body | string | false | 2022-07-10T00:00:00Z | |
| owner_id | The Distru user that owns this order, as a user ID (the same id in the response's owner.id). Optional. |
body | string | false | ||
| shipping_location_id | The shipping location's ID (a Distru location ID). Optional. | body | string | false | ||
| status | 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 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.PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
body | string | false | PENDING | |
| upsert_invoice | 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. | body | boolean | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single order | OrderResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Package
Finish packages
Success scenario
POST /public/v1/packages/finish
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzgsImlhdCI6MTc4NzU4NzI3OCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMTdiMzEwNDYtZTY1Ni00Y2MwLWJjZWYtNWE4ZTVlODI3OTE1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3Mjc3LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDAxOCIsInR5cCI6ImFjY2VzcyJ9.eAGYyOyV1P3au0gKyhY6V2Hpd1g79LIelzmbSEiAfdQ
{
"finished_datetime": "2026-08-01T00:00:00Z",
"package_ids": [
"00000000-0000-0000-0000-0000000000a4",
"00000000-0000-0000-0000-0000000000a5"
]
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 7afcca8ee54599c593368d981fb371d0-bd68a659eb807ee1-0
{
"data": [
{
"batch_number": null,
"biotrack_id": null,
"biotrack_inventory_type_id": null,
"biotrack_net_quantity_per_unit": null,
"biotrack_room_id": null,
"biotrack_status": null,
"biotrack_usable_weight": null,
"compliance_label": "ABCDEF012345670000000312",
"compliance_product_name": "Buds",
"compliance_strain_name": "Cotton Candy",
"compliance_transferred_datetime": null,
"compliance_type": "METRC",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-3998@example.com",
"full_name": "FirstName8106 LastName8107",
"id": "00000000-0000-0000-0000-000000000fb9",
"inserted_datetime": "2026-08-24T16:01:18.756372Z",
"role": {
"id": "00000000-0000-0000-0000-000000000ff0",
"name": "Admin 4079"
}
},
"custom_data": [],
"description": null,
"distru_status": "FINISHED",
"expiration_date": null,
"expiration_datetime": null,
"finished_datetime": "2026-08-01T00:00:00.000000Z",
"harvest_date": null,
"id": "00000000-0000-0000-0000-0000000000a4",
"inactivated_datetime": null,
"inserted_datetime": "2026-08-24T16:01:18.775737Z",
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": false,
"lab_testing_state": "NotSubmitted",
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:18.717321Z",
"id": "00000000-0000-0000-0000-0000000000e1",
"inserted_datetime": "2026-08-24T16:01:18.717403Z",
"issue_datetime": "2026-08-24T16:01:18.717319Z",
"license_number": "CDPH-00000228",
"license_type": "Specialty Outdoor"
},
"license_id": "00000000-0000-0000-0000-0000000000e1",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000b24",
"id": "00000000-0000-0000-0000-000000000369",
"license_id": "00000000-0000-0000-0000-0000000000e1",
"name": "Place 871"
},
"location_id": "00000000-0000-0000-0000-000000000369",
"metrc_archived_date": null,
"metrc_finished_date": null,
"metrc_id": 312,
"metrc_label": "ABCDEF012345670000000312",
"metrc_production_batch_number": null,
"metrc_received_datetime": null,
"metrc_received_from_manifest_number": null,
"metrc_source_harvest_names": null,
"metrc_status": "ACTIVE",
"metrc_transfer_id": null,
"metrc_unit_name": "Ounces",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-3995@example.com",
"full_name": "FirstName8100 LastName8101",
"id": "00000000-0000-0000-0000-000000000fb6",
"inserted_datetime": "2026-08-24T16:01:18.729358Z",
"role": {
"id": "00000000-0000-0000-0000-000000000fee",
"name": "Admin 4077"
}
},
"packaged_date": "2014-11-29",
"primary_test_result": null,
"product": {
"id": "dc238f1f-7fd3-4374-95b5-d2832f339821",
"name": "Product 2932",
"sku": "sku 2933",
"updated_datetime": "2026-08-24T16:01:18.740712Z"
},
"product_id": "dc238f1f-7fd3-4374-95b5-d2832f339821",
"product_unit_quantity": "0.000000000",
"product_unit_type": {
"id": "00000000-0000-0000-0000-000000008de1",
"name": "Ounce"
},
"quantity": "0.000000000",
"quantity_active": "0.000000000",
"quantity_assembling": "0.000000000",
"quantity_available": "0.000000000",
"status": "finished",
"unit_type": {
"id": "00000000-0000-0000-0000-000000008de1",
"name": "Ounce"
}
},
{
"batch_number": null,
"biotrack_id": null,
"biotrack_inventory_type_id": null,
"biotrack_net_quantity_per_unit": null,
"biotrack_room_id": null,
"biotrack_status": null,
"biotrack_usable_weight": null,
"compliance_label": "ABCDEF012345670000000314",
"compliance_product_name": "Buds",
"compliance_strain_name": "Cotton Candy",
"compliance_transferred_datetime": null,
"compliance_type": "METRC",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-4015@example.com",
"full_name": "FirstName8140 LastName8141",
"id": "00000000-0000-0000-0000-000000000fcb",
"inserted_datetime": "2026-08-24T16:01:18.828886Z",
"role": {
"id": "00000000-0000-0000-0000-000000001003",
"name": "Admin 4098"
}
},
"custom_data": [],
"description": null,
"distru_status": "FINISHED",
"expiration_date": null,
"expiration_datetime": null,
"finished_datetime": "2026-08-01T00:00:00.000000Z",
"harvest_date": null,
"id": "00000000-0000-0000-0000-0000000000a5",
"inactivated_datetime": null,
"inserted_datetime": "2026-08-24T16:01:18.848125Z",
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": false,
"lab_testing_state": "NotSubmitted",
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:18.717321Z",
"id": "00000000-0000-0000-0000-0000000000e1",
"inserted_datetime": "2026-08-24T16:01:18.717403Z",
"issue_datetime": "2026-08-24T16:01:18.717319Z",
"license_number": "CDPH-00000228",
"license_type": "Specialty Outdoor"
},
"license_id": "00000000-0000-0000-0000-0000000000e1",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000b24",
"id": "00000000-0000-0000-0000-00000000036d",
"license_id": "00000000-0000-0000-0000-0000000000e1",
"name": "Place 875"
},
"location_id": "00000000-0000-0000-0000-00000000036d",
"metrc_archived_date": null,
"metrc_finished_date": null,
"metrc_id": 314,
"metrc_label": "ABCDEF012345670000000314",
"metrc_production_batch_number": null,
"metrc_received_datetime": null,
"metrc_received_from_manifest_number": null,
"metrc_source_harvest_names": null,
"metrc_status": "ACTIVE",
"metrc_transfer_id": null,
"metrc_unit_name": "Ounces",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-4007@example.com",
"full_name": "FirstName8124 LastName8125",
"id": "00000000-0000-0000-0000-000000000fc1",
"inserted_datetime": "2026-08-24T16:01:18.804273Z",
"role": {
"id": "00000000-0000-0000-0000-000000000ffa",
"name": "Admin 4089"
}
},
"packaged_date": "2014-11-29",
"primary_test_result": null,
"product": {
"id": "f17bfc14-3830-4b8c-9b15-e1bc4cd75f3a",
"name": "Product 2947",
"sku": "sku 2948",
"updated_datetime": "2026-08-24T16:01:18.816331Z"
},
"product_id": "f17bfc14-3830-4b8c-9b15-e1bc4cd75f3a",
"product_unit_quantity": "0.000000000",
"product_unit_type": {
"id": "00000000-0000-0000-0000-000000008de1",
"name": "Ounce"
},
"quantity": "0.000000000",
"quantity_active": "0.000000000",
"quantity_assembling": "0.000000000",
"quantity_available": "0.000000000",
"status": "finished",
"unit_type": {
"id": "00000000-0000-0000-0000-000000008de1",
"name": "Ounce"
}
}
],
"next_page": null
}
Error scenario: Metrc connection not established
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 5a3b8923521647e3fd63509ee2bb5651-57f63b98fd9c518d-0
{
"errors": [
{
"context": {},
"message": "Your Metrc connection is not properly established",
"pointer": [
"base"
]
}
]
}
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.
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.
Every package must be finishable, or the whole request fails. A package is rejected when it: • still has active quantity — a package must already be at zero quantity before it can be finished • is already finished • is discontinued, on hold, inactive, or has been transferred out of its license • is still syncing with Metrc from an earlier operation • has an unresolved Metrc compliance discrepancy
This 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.
Metrc 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.
Required permission: products_permissions_adjust_inventory.
Request
POST /public/v1/packages/finish
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| finished_datetime | 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. |
body | string | false | ||
| package_ids | 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. |
body | array(string) | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The finished packages | Packages |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get packages
Success scenario
GET /public/v1/packages
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYjJjMjQ5NzktODMwMi00Y2I0LTgxMDktZTRjNDNiYmM5YjE0IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTM4OCIsInR5cCI6ImFjY2VzcyJ9.F9mdsWaDg5x7eEu-CyyqURCHtlO1zqai-d5pqdJARH0
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 60700ead194da0fd5474934a8934245e-6a752ee0bd222b64-0
{
"data": [
{
"batch_number": null,
"biotrack_id": null,
"biotrack_inventory_type_id": null,
"biotrack_net_quantity_per_unit": null,
"biotrack_room_id": null,
"biotrack_status": null,
"biotrack_usable_weight": null,
"compliance_label": "ABCDEF012345670000000056",
"compliance_product_name": "Buds",
"compliance_strain_name": "Cotton Candy",
"compliance_transferred_datetime": null,
"compliance_type": "METRC",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1418@example.com",
"full_name": "FirstName2874 LastName2875",
"id": "00000000-0000-0000-0000-00000000058c",
"inserted_datetime": "2026-08-24T16:01:07.848346Z",
"role": {
"id": "00000000-0000-0000-0000-0000000005b1",
"name": "Admin 1456"
}
},
"custom_data": [
{
"id": 35,
"name": "Custom Field 28",
"value": "Custom Field Value 1"
}
],
"description": null,
"distru_status": "ACTIVE",
"expiration_date": "2024-01-01T00:00:00.000000Z",
"expiration_datetime": "2024-01-01T00:00:00.000000Z",
"finished_datetime": null,
"harvest_date": "2024-06-15",
"id": "00000000-0000-0000-0000-000000000020",
"inactivated_datetime": null,
"inserted_datetime": "2026-08-24T16:01:07.895429Z",
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": true,
"lab_testing_state": "NotSubmitted",
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:07.722705Z",
"id": "00000000-0000-0000-0000-000000000033",
"inserted_datetime": "2026-08-24T16:01:07.722802Z",
"issue_datetime": "2026-08-24T16:01:07.722704Z",
"license_number": "CDPH-00000052",
"license_type": "Specialty Cottage Indoor"
},
"license_id": "00000000-0000-0000-0000-000000000033",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000419",
"id": "00000000-0000-0000-0000-00000000011d",
"license_id": "00000000-0000-0000-0000-000000000033",
"name": "Place 284"
},
"location_id": "00000000-0000-0000-0000-00000000011d",
"metrc_archived_date": null,
"metrc_finished_date": null,
"metrc_id": 56,
"metrc_label": "ABCDEF012345670000000056",
"metrc_production_batch_number": null,
"metrc_received_datetime": null,
"metrc_received_from_manifest_number": null,
"metrc_source_harvest_names": null,
"metrc_status": "ACTIVE",
"metrc_transfer_id": null,
"metrc_unit_name": "Ounces",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-1393@example.com",
"full_name": "FirstName2826 LastName2827",
"id": "00000000-0000-0000-0000-000000000574",
"inserted_datetime": "2026-08-24T16:01:07.760978Z",
"role": {
"id": "00000000-0000-0000-0000-000000000598",
"name": "Admin 1431"
}
},
"packaged_date": "2024-07-01",
"primary_test_result": null,
"product": {
"id": "ea2fd509-535d-441c-9778-91ac68a3188a",
"name": "Product 515",
"sku": "sku 516",
"updated_datetime": "2026-08-24T16:01:07.789133Z"
},
"product_id": "ea2fd509-535d-441c-9778-91ac68a3188a",
"product_unit_quantity": "7.500000000",
"product_unit_type": {
"id": "00000000-0000-0000-0000-000000003394",
"name": "3"
},
"quantity": "5.000000000",
"quantity_active": "5.000000000",
"quantity_assembling": "0.000000000",
"quantity_available": "5.000000000",
"status": "active",
"unit_type": {
"id": "00000000-0000-0000-0000-000000003395",
"name": "2"
}
},
{
"batch_number": null,
"biotrack_id": null,
"biotrack_inventory_type_id": null,
"biotrack_net_quantity_per_unit": null,
"biotrack_room_id": null,
"biotrack_status": null,
"biotrack_usable_weight": null,
"compliance_label": "ABCDEF012345670000000060",
"compliance_product_name": "Buds",
"compliance_strain_name": "Cotton Candy",
"compliance_transferred_datetime": null,
"compliance_type": "METRC",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1483@example.com",
"full_name": "FirstName3006 LastName3007",
"id": "00000000-0000-0000-0000-0000000005cf",
"inserted_datetime": "2026-08-24T16:01:08.029188Z",
"role": {
"id": "00000000-0000-0000-0000-0000000005f1",
"name": "Admin 1520"
}
},
"custom_data": [
{
"id": 35,
"name": "Custom Field 28",
"value": null
}
],
"description": null,
"distru_status": "ACTIVE",
"expiration_date": null,
"expiration_datetime": null,
"finished_datetime": null,
"harvest_date": null,
"id": "00000000-0000-0000-0000-000000000023",
"inactivated_datetime": null,
"inserted_datetime": "2026-08-24T16:01:08.079480Z",
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": false,
"lab_testing_state": "NotSubmitted",
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:07.722705Z",
"id": "00000000-0000-0000-0000-000000000033",
"inserted_datetime": "2026-08-24T16:01:07.722802Z",
"issue_datetime": "2026-08-24T16:01:07.722704Z",
"license_number": "CDPH-00000052",
"license_type": "Specialty Cottage Indoor"
},
"license_id": "00000000-0000-0000-0000-000000000033",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000419",
"id": "00000000-0000-0000-0000-000000000130",
"license_id": "00000000-0000-0000-0000-000000000033",
"name": "Place 303"
},
"location_id": "00000000-0000-0000-0000-000000000130",
"metrc_archived_date": null,
"metrc_finished_date": null,
"metrc_id": 60,
"metrc_label": "ABCDEF012345670000000060",
"metrc_production_batch_number": null,
"metrc_received_datetime": null,
"metrc_received_from_manifest_number": null,
"metrc_source_harvest_names": null,
"metrc_status": "ACTIVE",
"metrc_transfer_id": null,
"metrc_unit_name": "Ounces",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-1393@example.com",
"full_name": "FirstName2826 LastName2827",
"id": "00000000-0000-0000-0000-000000000574",
"inserted_datetime": "2026-08-24T16:01:07.760978Z",
"role": {
"id": "00000000-0000-0000-0000-000000000598",
"name": "Admin 1431"
}
},
"packaged_date": null,
"primary_test_result": {
"cbd_mg_per_unit": "1",
"cbd_mg_per_unit_total": "2",
"cbd_percentage": "3",
"cbd_percentage_total": "4",
"coa_url": null,
"mg_per_unit_type": "mg/mL",
"name": "File.pdf",
"thc_mg_per_unit": "5",
"thc_mg_per_unit_total": "6",
"thc_percentage": "7",
"thc_percentage_total": "8"
},
"product": {
"id": "ea2fd509-535d-441c-9778-91ac68a3188a",
"name": "Product 515",
"sku": "sku 516",
"updated_datetime": "2026-08-24T16:01:07.789133Z"
},
"product_id": "ea2fd509-535d-441c-9778-91ac68a3188a",
"product_unit_quantity": "15.000000000",
"product_unit_type": {
"id": "00000000-0000-0000-0000-000000003394",
"name": "3"
},
"quantity": "20.000000000",
"quantity_active": "20.000000000",
"quantity_assembling": "0.000000000",
"quantity_available": "20.000000000",
"status": "active",
"unit_type": {
"id": "00000000-0000-0000-0000-00000000339f",
"name": "4"
}
}
],
"next_page": null
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 63c5adfb834bd9c4b6dfc9a14dfed464-ed7619d5679bc132-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"updated_datetime"
],
"section": "query"
}
]
}
Get packages sorted by their creation date and filtered by various attributes
This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.
Required permission: products_permissions_view. Results are filtered to
only include packages the authenticated user can access under their team
restrictions.
Request
GET /public/v1/packages
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_number | Case-insensitive substring match on the package's batch_number. |
query | string | false | ||
| batch_numbers | 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. |
query | array | false | ||
| bin_ids | Keep only packages stored in any of these bins (matches an id in a package's bins[].id). Multiple ids are OR-ed. |
query | array | false | ||
| compliance_label | Case-insensitive substring match on the package's compliance_label. |
query | string | false | ||
| compliance_labels | 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. |
query | array | false | ||
| compliance_product_name | Case-insensitive substring match on the package's compliance_product_name. |
query | string | false | ||
| compliance_product_names | Keep only packages whose compliance_product_name exactly matches (case-insensitive) any value in the list. Multiple values are OR-ed. |
query | array | false | ||
| contains_remediated_material | Keep only packages that contain remediated material (true) or do not (false). Omit to match either. |
query | boolean | false | ||
| custom_data | 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. |
query | object | false | ?custom_data[101]=Blue&custom_data[102]=Wholesale | |
| distru_statuses | 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:
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.) |
query | array | false | ?distru_statuses[]=ACTIVE&distru_statuses[]=SELLING&distru_statuses[]=SOLD | |
| expiration_datetime | 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. |
query | string | false | ?expiration_datetime=2026-01-01T00:00:00Z,2026-12-31T00:00:00Z | |
| finished_datetime | 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. |
query | string | false | ?finished_datetime=2026-01-01T00:00:00Z, | |
| harvest_date | 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. |
query | string | false | ?harvest_date=2026-01-01,2026-12-31 | |
| has_coa_attached | 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. |
query | boolean | false | ||
| has_quantity_active | Keep only packages that currently hold active quantity (true, quantity_active > 0) or none (false, quantity_active = 0). Omit to match either. |
query | boolean | false | ||
| ids | 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. |
query | array | false | ?ids[]=00000000-0000-0000-0000-000000000001&ids[]=00000000-0000-0000-0000-000000000002 | |
| inactivated_datetime | 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. |
query | string | false | ?inactivated_datetime=,2026-12-31T00:00:00Z | |
| include_costs | 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. |
query | boolean | false | ?include_costs=true | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| is_production_batch | Keep only production-batch (true) or non-production-batch (false) packages. Omit to match either. |
query | boolean | false | ||
| is_test_sample | Keep only test-sample (true) or non-test-sample (false) packages. Omit to match either. |
query | boolean | false | ||
| is_trade_sample | Keep only trade-sample (true) or non-trade-sample (false) packages. Omit to match either. |
query | boolean | false | ||
| lab_testing_state | Keep only packages whose lab_testing_state exactly equals this value (case-insensitive). |
query | string | false | ||
| lab_testing_states | Keep only packages whose lab_testing_state is any of these values (case-insensitive, OR-ed). |
query | array | false | ||
| license_ids | 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. |
query | array | false | ||
| license_number | Keep only packages whose license has exactly this license number (exact string match, not a substring). | query | string | false | ?license_number=1234567890 | |
| location_ids | 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. |
query | array | false | ?location_ids[]=c40e87ce-0647-409b-89fa-620275d77fcc&location_ids[]=65ca530a-1ea2-439b-b4c2-598abb1fc6f3 | |
| owner_ids | Keep only packages owned by any of these users (matches a package's owner.id). Multiple ids are OR-ed. |
query | array | false | ||
| packaged_date | 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. |
query | string | false | ?packaged_date=2026-01-01, | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| product_brand_ids | Keep only packages whose product has any of these brands (matches the package's product.brand.id). Multiple ids are OR-ed. |
query | array | false | ||
| product_category_ids | Keep only packages whose product is in any of these categories (matches the package's product.category.id). Multiple ids are OR-ed. |
query | array | false | ||
| product_group_ids | Keep only packages whose product is in any of these groups (matches the package's product.product_group.id). Multiple ids are OR-ed. |
query | array | false | ||
| product_ids | Keep only packages of any of these product IDs (matches a package's product_id). Multiple ids are OR-ed. |
query | array | false | ?product_ids[]=c40e87ce-0647-409b-89fa-620275d77fcc&product_ids[]=65ca530a-1ea2-439b-b4c2-598abb1fc6f3 | |
| product_skus | 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. |
query | array | false | ||
| product_strain_ids | Keep only packages whose product has any of these strains (matches the package's product.strain.id). Multiple ids are OR-ed. |
query | array | false | ||
| product_subcategory_ids | Keep only packages whose product is in any of these subcategories (matches the package's product.subcategory.id). Multiple ids are OR-ed. |
query | array | false | ||
| product_tag_ids | 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. |
query | array | false | ||
| product_vendor_ids | 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. |
query | array | false | ||
| unit_type_ids | Keep only packages with any of these unit types (matches a package's unit_type.id). Multiple ids are OR-ed. |
query | array | false | ||
| updated_datetime | 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. |
query | string | false | ?updated_datetime=,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of packages | Packages |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Move packages
Success scenario
POST /public/v1/packages/move
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiMDM1ZjA4ODUtZjYxZC00MGQwLWJmNGYtOWEzNDc4OTQ1ZjZjIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTE2MiIsInR5cCI6ImFjY2VzcyJ9.A-a7N9D8GIAoG97EnPY5nB7LSipDN1pfieNU0s-EhCE
{
"location_id": "00000000-0000-0000-0000-0000000000ef",
"package_ids": [
"00000000-0000-0000-0000-000000000017",
"00000000-0000-0000-0000-00000000001a"
]
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a12b5096169634c940b354c8a519f65b-3176a1bb34545ed6-0
{
"data": [
{
"batch_number": null,
"biotrack_id": null,
"biotrack_inventory_type_id": null,
"biotrack_net_quantity_per_unit": null,
"biotrack_room_id": null,
"biotrack_status": null,
"biotrack_usable_weight": null,
"compliance_label": "ABCDEF012345670000000036",
"compliance_product_name": "Buds",
"compliance_strain_name": "Cotton Candy",
"compliance_transferred_datetime": null,
"compliance_type": "METRC",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1190@example.com",
"full_name": "FirstName2420 LastName2421",
"id": "00000000-0000-0000-0000-0000000004a9",
"inserted_datetime": "2026-08-24T16:01:07.065100Z",
"role": {
"id": "00000000-0000-0000-0000-0000000004c8",
"name": "Admin 1223"
}
},
"custom_data": [],
"description": null,
"distru_status": "ACTIVE",
"expiration_date": null,
"expiration_datetime": null,
"finished_datetime": null,
"harvest_date": null,
"id": "00000000-0000-0000-0000-000000000017",
"inactivated_datetime": null,
"inserted_datetime": "2026-08-24T16:01:07.115738Z",
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": false,
"lab_testing_state": "NotSubmitted",
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:06.984189Z",
"id": "00000000-0000-0000-0000-000000000023",
"inserted_datetime": "2026-08-24T16:01:06.984311Z",
"issue_datetime": "2026-08-24T16:01:06.984185Z",
"license_number": "CDPH-00000036",
"license_type": "Type 7 Volatile Solvent Extraction"
},
"license_id": "00000000-0000-0000-0000-000000000023",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000388",
"id": "00000000-0000-0000-0000-0000000000ef",
"license_id": "00000000-0000-0000-0000-000000000023",
"name": "Place 238"
},
"location_id": "00000000-0000-0000-0000-0000000000ef",
"metrc_archived_date": null,
"metrc_finished_date": null,
"metrc_id": 36,
"metrc_label": "ABCDEF012345670000000036",
"metrc_production_batch_number": null,
"metrc_received_datetime": null,
"metrc_received_from_manifest_number": null,
"metrc_source_harvest_names": null,
"metrc_status": "ACTIVE",
"metrc_transfer_id": null,
"metrc_unit_name": "Ounces",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-1175@example.com",
"full_name": "FirstName2389 LastName2391",
"id": "00000000-0000-0000-0000-000000000499",
"inserted_datetime": "2026-08-24T16:01:07.016624Z",
"role": {
"id": "00000000-0000-0000-0000-0000000004b9",
"name": "Admin 1208"
}
},
"packaged_date": "2014-11-29",
"primary_test_result": null,
"product": {
"id": "e353d79f-26ee-40d4-ad47-6cdba811fd3f",
"name": "Product 415",
"sku": "sku 416",
"updated_datetime": "2026-08-24T16:01:07.036190Z"
},
"product_id": "e353d79f-26ee-40d4-ad47-6cdba811fd3f",
"product_unit_quantity": "5.000000000",
"product_unit_type": {
"id": "00000000-0000-0000-0000-000000002ba8",
"name": "Ounce"
},
"quantity": "5.000000000",
"quantity_active": "5.000000000",
"quantity_assembling": "0.000000000",
"quantity_available": "5.000000000",
"status": "active",
"unit_type": {
"id": "00000000-0000-0000-0000-000000002ba8",
"name": "Ounce"
}
},
{
"batch_number": null,
"biotrack_id": null,
"biotrack_inventory_type_id": null,
"biotrack_net_quantity_per_unit": null,
"biotrack_room_id": null,
"biotrack_status": null,
"biotrack_usable_weight": null,
"compliance_label": "ABCDEF012345670000000044",
"compliance_product_name": "Buds",
"compliance_strain_name": "Cotton Candy",
"compliance_transferred_datetime": null,
"compliance_type": "METRC",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1244@example.com",
"full_name": "FirstName2528 LastName2529",
"id": "00000000-0000-0000-0000-0000000004df",
"inserted_datetime": "2026-08-24T16:01:07.270819Z",
"role": {
"id": "00000000-0000-0000-0000-000000000501",
"name": "Admin 1280"
}
},
"custom_data": [],
"description": null,
"distru_status": "ACTIVE",
"expiration_date": null,
"expiration_datetime": null,
"finished_datetime": null,
"harvest_date": null,
"id": "00000000-0000-0000-0000-00000000001a",
"inactivated_datetime": null,
"inserted_datetime": "2026-08-24T16:01:07.315302Z",
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": false,
"lab_testing_state": "NotSubmitted",
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:06.984189Z",
"id": "00000000-0000-0000-0000-000000000023",
"inserted_datetime": "2026-08-24T16:01:06.984311Z",
"issue_datetime": "2026-08-24T16:01:06.984185Z",
"license_number": "CDPH-00000036",
"license_type": "Type 7 Volatile Solvent Extraction"
},
"license_id": "00000000-0000-0000-0000-000000000023",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000388",
"id": "00000000-0000-0000-0000-0000000000ef",
"license_id": "00000000-0000-0000-0000-000000000023",
"name": "Place 238"
},
"location_id": "00000000-0000-0000-0000-0000000000ef",
"metrc_archived_date": null,
"metrc_finished_date": null,
"metrc_id": 44,
"metrc_label": "ABCDEF012345670000000044",
"metrc_production_batch_number": null,
"metrc_received_datetime": null,
"metrc_received_from_manifest_number": null,
"metrc_source_harvest_names": null,
"metrc_status": "ACTIVE",
"metrc_transfer_id": null,
"metrc_unit_name": "Ounces",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-1225@example.com",
"full_name": "FirstName2492 LastName2493",
"id": "00000000-0000-0000-0000-0000000004cd",
"inserted_datetime": "2026-08-24T16:01:07.226677Z",
"role": {
"id": "00000000-0000-0000-0000-0000000004ec",
"name": "Admin 1259"
}
},
"packaged_date": "2014-11-29",
"primary_test_result": null,
"product": {
"id": "52947fd2-1075-46ce-af19-c8ce96c1eda0",
"name": "Product 434",
"sku": "sku 435",
"updated_datetime": "2026-08-24T16:01:07.250261Z"
},
"product_id": "52947fd2-1075-46ce-af19-c8ce96c1eda0",
"product_unit_quantity": "3.000000000",
"product_unit_type": {
"id": "00000000-0000-0000-0000-000000002ba8",
"name": "Ounce"
},
"quantity": "3.000000000",
"quantity_active": "3.000000000",
"quantity_assembling": "0.000000000",
"quantity_available": "3.000000000",
"status": "active",
"unit_type": {
"id": "00000000-0000-0000-0000-000000002ba8",
"name": "Ounce"
}
}
],
"next_page": null
}
Error scenario: package has no active quantity
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 2bf6683a3301af4c0b90bde962a07a5e-a14b68e349e8883a-0
{
"errors": [
{
"context": {},
"message": "All packages must have positive active quantity",
"pointer": [
"base"
]
}
]
}
Move a list of packages to a destination location within a single license.
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.
Each 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.
This 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.
Not 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.
Required permission: products_permissions_adjust_inventory.
Request
POST /public/v1/packages/move
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| location_id | 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. |
body | string | true | ||
| metrc_location_id | 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. | body | string | false | ||
| package_ids | 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. |
body | array(string) | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The moved packages | Packages |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Update a package
Success scenario
POST /public/v1/packages/00000000-0000-0000-0000-000000000048
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzEsImlhdCI6MTc4NzU4NzI3MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZTg3YjNjNTYtNDRmMy00ZWYwLWE2MDgtMzEzNWFkNzgzN2Q1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjYyNiIsInR5cCI6ImFjY2VzcyJ9.CEHJKY-0FCy1AbIN5Z18e5_XgLRNukTvxZFrfNIHwYY
{
"batch_number": "NEW-BATCH-001",
"custom_data": {
"88": "Updated Value"
},
"description": "Updated description",
"expiration_datetime": "2025-02-02T03:04:05.000000Z",
"harvest_date": "2025-03-03"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: bfee1ed8254539aa51bd987f541937c5-647c32477a2645d8-0
{
"data": {
"batch_number": "NEW-BATCH-001",
"biotrack_id": null,
"biotrack_inventory_type_id": null,
"biotrack_net_quantity_per_unit": null,
"biotrack_room_id": null,
"biotrack_status": null,
"biotrack_usable_weight": null,
"compliance_label": "ABCDEF012345670000000135",
"compliance_product_name": "Buds",
"compliance_strain_name": "Cotton Candy",
"compliance_transferred_datetime": null,
"compliance_type": "METRC",
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-2639@example.com",
"full_name": "FirstName5374 LastName5375",
"id": "00000000-0000-0000-0000-000000000a5c",
"inserted_datetime": "2026-08-24T16:01:11.711994Z",
"role": {
"id": "00000000-0000-0000-0000-000000000a94",
"name": "Admin 2707"
}
},
"custom_data": [
{
"id": 88,
"name": "Custom Field 62",
"value": "Updated Value"
}
],
"description": "Updated description",
"distru_status": "ACTIVE",
"expiration_date": "2025-02-02T03:04:05.000000Z",
"expiration_datetime": "2025-02-02T03:04:05.000000Z",
"finished_datetime": null,
"harvest_date": "2025-03-03",
"id": "00000000-0000-0000-0000-000000000048",
"inactivated_datetime": null,
"inserted_datetime": "2026-08-24T16:01:11.752244Z",
"is_production_batch": false,
"is_test_sample": false,
"is_trade_sample": false,
"lab_testing_state": "NotSubmitted",
"license": {
"active": true,
"expiry_datetime": "2026-09-24T16:01:11.630614Z",
"id": "00000000-0000-0000-0000-000000000068",
"inserted_datetime": "2026-08-24T16:01:11.630767Z",
"issue_datetime": "2026-08-24T16:01:11.630607Z",
"license_number": "CDPH-00000107",
"license_type": "Type 8 Testing"
},
"license_id": "00000000-0000-0000-0000-000000000068",
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-00000000073c",
"id": "00000000-0000-0000-0000-0000000001f4",
"license_id": "00000000-0000-0000-0000-000000000068",
"name": "Place 499"
},
"location_id": "00000000-0000-0000-0000-0000000001f4",
"metrc_archived_date": null,
"metrc_finished_date": null,
"metrc_id": 135,
"metrc_label": "ABCDEF012345670000000135",
"metrc_production_batch_number": null,
"metrc_received_datetime": null,
"metrc_received_from_manifest_number": null,
"metrc_source_harvest_names": null,
"metrc_status": "ACTIVE",
"metrc_transfer_id": null,
"metrc_unit_name": "Ounces",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-2623@example.com",
"full_name": "FirstName5342 LastName5343",
"id": "00000000-0000-0000-0000-000000000a4c",
"inserted_datetime": "2026-08-24T16:01:11.660023Z",
"role": {
"id": "00000000-0000-0000-0000-000000000a82",
"name": "Admin 2689"
}
},
"packaged_date": "2014-11-29",
"primary_test_result": null,
"product": {
"id": "6299a24d-124e-4948-b0bf-6fb6ce729219",
"name": "Product 1304",
"sku": "sku 1305",
"updated_datetime": "2026-08-24T16:01:11.686554Z"
},
"product_id": "6299a24d-124e-4948-b0bf-6fb6ce729219",
"product_unit_quantity": "141.747462720",
"product_unit_type": {
"id": "00000000-0000-0000-0000-000000005a67",
"name": "Gram"
},
"quantity": "5.000000000",
"quantity_active": "5.000000000",
"quantity_assembling": "0.000000000",
"quantity_available": "5.000000000",
"status": "active",
"unit_type": {
"id": "00000000-0000-0000-0000-000000005a69",
"name": "Ounce"
}
}
}
Error scenario: invalid expiration_datetime
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: e499662bb55784bc774525f26917b2c4-637145415f2c423b-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"expiration_datetime"
],
"section": "body"
}
]
}
Update the Distru-tracked fields of an existing package. Packages cannot be created through the API, only updated.
These 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.
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.
Supports 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.
Required permission: products_permissions_edit.
Request
POST /public/v1/packages/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_number | The package's batch number. Sparse: omit to leave unchanged; send null to clear. |
body | string | false | ||
| bin_ids | 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. |
body | array | false | ||
| custom_data | 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. |
body | object | false | {"101":"Some text value","102":"2026-08-18T00:00:00.000-07:00","103":["Option A","Option B"]} | |
| description | Free-form text describing the package. Sparse: omit to leave unchanged; send null to clear. |
body | string | false | ||
| expiration_datetime | 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. |
body | string | false | ||
| harvest_date | The package's harvest date, YYYY-MM-DD (e.g. 2026-08-01). Sparse: omit to leave unchanged; send null to clear. |
body | string | false | ||
| id | The package's ID (the id string returned by GET /public/v1/packages). |
path | string | true | ||
| is_inactive | 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). |
body | boolean | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated package | PackageFullResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Payment
Get a payment
Success scenario
GET /public/v1/payments/00000000-0000-0000-0000-000000000024
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzAsImlhdCI6MTc4NzU4NzI3MCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMTFmNTMzNjctMDc3NS00YTkyLWI4NDMtOWM2OWNmYzhmNWE2IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY5LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjE4MSIsInR5cCI6ImFjY2VzcyJ9.Nlsxp5M1xJatoCQhtJc2rGFurVzpxAf9u1WgvqE1-KY
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c8ceb65045aa98792b419bd46a059418-fe77cf1ec57a543d-0
{
"data": {
"amount": "10",
"company": {
"id": "00000000-0000-0000-0000-0000000002a7",
"name": "Company 1557",
"updated_datetime": "2026-08-24T16:01:10.102256Z"
},
"credit_uses": [
{
"amount": "30",
"credit": {
"amount": "100",
"credit_number": "CRT-U",
"id": "bb27673b-26ca-463d-8327-8732c4436300",
"source": "USER"
},
"id": "b80c8e39-24cd-4efb-bac0-7af44efe55d3"
}
],
"description": null,
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-000000000024",
"inserted_datetime": "2026-08-24T16:01:10.121777Z",
"invoice": {
"id": "00000000-0000-0000-0000-00000000002e",
"invoice_number": "Invoice #44",
"status": "NOT_PAID",
"total": "32.00"
},
"overpayment_credits": [
{
"amount": "20",
"credit_number": "CRT-OP",
"id": "cd953984-6275-4d8d-adf3-3e03f159709e",
"source": "INVOICE_PAYMENT"
}
],
"payment_date": "2026-08-24T16:01:10.120520Z",
"payment_datetime": "2026-08-24T16:01:10.120520Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000032",
"inserted_datetime": "2026-08-24T16:01:10.119863Z",
"name": "Payment Method 49",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:10.119863Z"
},
"payment_number": "Payment #34",
"payment_type": "INVOICE",
"purchase": null,
"quickbooks_deposit_account_id": null,
"quickbooks_deposit_account_name": null,
"status": "POSTED",
"updated_datetime": "2026-08-24T16:01:10.121777Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f798cfb30229128f5b76aca8e07bcae5-29074918695bcccd-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get a single payment by its ID.
The 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.
Returns 404 if no payment with that ID exists within your company. Voided payments are returned normally with status VOIDED; soft-deleted payments return 404.
Required permission: payments_permissions_view.
Request
GET /public/v1/payments/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The payment's Distru ID, as returned in the id field of the list endpoint. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single payment | PaymentResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get payments
Success scenario
GET /public/v1/payments
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOWQ5ODgwMWItMjFhMi00YTAwLWI4NTUtYjE2MjkzZWY3YmIwIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODM5IiwidHlwIjoiYWNjZXNzIn0.D8rU_WVCthSynBcKuQGnPFjcLfqdHqok4m8wyw0jy_M
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: d8c77c1f2ca62fc4272c898954ac776b-b807afaef6c261f4-0
{
"data": [
{
"amount": "75.25",
"company": {
"id": "00000000-0000-0000-0000-0000000000f4",
"name": "Company 706",
"updated_datetime": "2026-08-24T16:01:05.993867Z"
},
"credit_uses": null,
"description": "pur payment",
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-000000000005",
"inserted_datetime": "2026-08-24T16:01:06.026947Z",
"invoice": null,
"overpayment_credits": null,
"payment_date": "2026-08-24T16:01:06.025787Z",
"payment_datetime": "2026-08-24T16:01:06.025787Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000013",
"inserted_datetime": "2026-08-24T16:01:06.024649Z",
"name": "Payment Method 18",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:06.024649Z"
},
"payment_number": "Payment #4",
"payment_type": "PURCHASE",
"purchase": {
"id": "00000000-0000-0000-0000-00000000000a",
"purchase_number": "Purchase #9",
"status": "PENDING",
"total": "32.00"
},
"quickbooks_deposit_account_id": null,
"status": "POSTED",
"updated_datetime": "2026-08-24T16:01:06.026947Z"
},
{
"amount": "150.5",
"company": {
"id": "00000000-0000-0000-0000-0000000000f0",
"name": "Company 691",
"updated_datetime": "2026-08-24T16:01:05.939513Z"
},
"credit_uses": [
{
"amount": "30",
"credit": {
"amount": "100",
"credit_number": "CRT-U",
"id": "742f6f7c-7ef0-460f-b973-2688472c91a3",
"source": "USER"
},
"id": "8011ef98-305b-4f6e-8239-51d30460cc71"
}
],
"description": "inv payment",
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-000000000004",
"inserted_datetime": "2026-08-24T16:01:05.965028Z",
"invoice": {
"id": "00000000-0000-0000-0000-00000000000b",
"invoice_number": "Invoice #10",
"status": "NOT_PAID",
"total": "32.00"
},
"overpayment_credits": [
{
"amount": "20",
"credit_number": "CRT-OP",
"id": "c1c44171-31d0-435a-9979-ec74269f7ced",
"source": "INVOICE_PAYMENT"
}
],
"payment_date": "2026-08-24T16:01:05.963869Z",
"payment_datetime": "2026-08-24T16:01:05.963869Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000012",
"inserted_datetime": "2026-08-24T16:01:05.962102Z",
"name": "Payment Method 17",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:05.962102Z"
},
"payment_number": "Payment #3",
"payment_type": "INVOICE",
"purchase": null,
"quickbooks_deposit_account_id": null,
"status": "POSTED",
"updated_datetime": "2026-08-24T16:01:05.965028Z"
}
],
"next_page": null
}
Error scenario: invalid payment_status filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f30f5f03a0765034a8f2d917eefc45d3-39aafea9dbef80f6-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"payment_status"
],
"section": "query"
}
]
}
Get a paginated list of your company's payments, newest first (ordered by creation time, then id).
A payment is a single record of money moving, and comes in one of two kinds, reported in payment_type:
• INVOICE — money received from a customer against an invoice. The invoice field is populated and purchase is null.
• PURCHASE — money paid to a vendor against a purchase. The purchase field is populated and invoice is null.
Exactly 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).
Each 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.
Narrow 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).
Results 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.
Required permission: payments_permissions_view.
Request
GET /public/v1/payments
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| amount | 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. |
query | string | false | ?amount=100,500 | |
| company_ids | 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. |
query | array | false | ?company_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z, | |
| invoice_ids | 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. |
query | array | false | ?invoice_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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). |
query | number | false | ?page[number]=1 | |
| payment_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z, | |
| payment_method_ids | 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. |
query | array | false | ?payment_method_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| payment_number | Filter to payments whose payment_number contains this value (case-insensitive substring match, not an exact match). |
query | string | false | ||
| payment_status | Filter by payment status. POSTED returns only non-voided payments; VOIDED returns only voided ones. Omit to return both POSTED and VOIDED payments. POSTED VOIDED |
query | string | false | ||
| payment_type | 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. INVOICE PURCHASE |
query | string | false | ||
| purchase_ids | 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. |
query | array | false | ?purchase_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| updated_datetime | 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. |
query | string | false | ,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of payments | Payments |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Void a payment
Success scenario
POST /public/v1/payments/00000000-0000-0000-0000-000000000021/void
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjksImlhdCI6MTc4NzU4NzI2OSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiY2Y1OTQ5MTMtMTM4ZS00NWMzLTliZGYtMDRiYzJhNmZlNTAyIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY4LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjA1NSIsInR5cCI6ImFjY2VzcyJ9.PhlcyBghLYBTsOVTuytoFx5aC3Kurk6QbBfsS5O1ZT8
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 15d32fd8ea21678fc3907eeec03a247f-2bf441c4bdc60f22-0
{
"data": {
"amount": "10",
"company": {
"id": "00000000-0000-0000-0000-00000000027a",
"name": "Company 1477",
"updated_datetime": "2026-08-24T16:01:09.675970Z"
},
"credit_uses": [],
"description": null,
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-000000000021",
"inserted_datetime": "2026-08-24T16:01:09.704170Z",
"invoice": {
"id": "00000000-0000-0000-0000-00000000002d",
"invoice_number": "Invoice #43",
"status": "FULLY_PAID",
"total": "32.00"
},
"overpayment_credits": [],
"payment_date": "2026-08-24T16:01:09.703094Z",
"payment_datetime": "2026-08-24T16:01:09.703094Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000030",
"inserted_datetime": "2026-08-24T16:01:09.700970Z",
"name": "Payment Method 47",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:09.700970Z"
},
"payment_number": "Payment #32",
"payment_type": "INVOICE",
"purchase": null,
"quickbooks_deposit_account_id": null,
"quickbooks_deposit_account_name": null,
"status": "VOIDED",
"updated_datetime": "2026-08-24T16:01:09.704170Z"
}
}
Error scenario: payment already voided
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 5bc026d39d30680e884875e0e6556975-59ce2b2d4d9c38c6-0
{
"errors": [
{
"context": {},
"message": "Payment is already voided",
"pointer": [
"base"
]
}
]
}
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.
This one endpoint voids both kinds of payment; it dispatches on payment_type:
• INVOICE — reverses the money applied to the invoice, 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.
• PURCHASE — reverses the money applied to the purchase and recomputes the purchase's payment status. Requires the purchases_permissions_make_payments permission.
Voiding 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.
Request
POST /public/v1/payments/{id}/void
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The voided payment | PaymentResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
PaymentMethod
Get a payment method
Success scenario
GET /public/v1/payment-methods/00000000-0000-0000-0000-000000000001
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMmM0NzYwZmMtMjg3Zi00NjA3LWE5ZDEtYWY4NGEzYjI1NDUxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NTQiLCJ0eXAiOiJhY2Nlc3MifQ.givGVPPMBddYfqTT5qTKs9f5FB9CYe5yTAKhdwipE-8
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: fd1b8985c210aa4f4411b9dab3b2ceb7-4e13bcc9476a53fe-0
{
"data": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000001",
"inserted_datetime": "2026-08-24T16:01:02.196093Z",
"name": "Cash",
"qb_payment_method_id": null,
"type": "CASH",
"updated_datetime": "2026-08-24T16:01:02.196093Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cf2785337aeb71234e8ed70bbdc19b56-f87d6f811341b1a7-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Fetch a single payment method by its ID. Use this to resolve the method a
payment references, or to read one method's current name, type, active flag, and QuickBooks
Online mapping (qb_payment_method_id).
Returns 404 if no payment method with that ID exists within your company — the lookup is scoped to
your company, so an ID belonging to another company reads as not found. A soft-deleted method is
still returned here (with its deleted_at set); deletion hides a method from new use but does not
remove it.
Required permission: settings_permissions_payment_methods.
Request
GET /public/v1/payment-methods/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single payment method | PaymentMethodResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get payment methods
Success scenario
GET /public/v1/payment-methods
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZDNjYTQ1ZDMtM2Q5ZC00YzBlLWE3MWItYjhjZjc1NDc1YTM0IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODYiLCJ0eXAiOiJhY2Nlc3MifQ.iMcxZrJCpeI1SzvyHSXpMqBcTtqbn80zdgia9QoMMJw
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c89627ecb4e152745ad83ec5bb5eefbc-fd96382beb035a42-0
{
"data": [
{
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000002",
"inserted_datetime": "2026-08-24T16:01:02.585894Z",
"name": "Payment Method 1",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:02.585894Z"
}
],
"next_page": null
}
Error scenario: invalid deleted filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 9b668ad190446b57b294501e6e01604f-f3033e6de382c467-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"deleted"
],
"section": "query"
}
]
}
List the payment methods configured for your company. A payment method
is how money changes hands on a payment — its type is one of CASH, CHECK, CREDIT_CARD,
BANK_REMITTANCE, or BANK_TRANSFER, and each method has a company-chosen name on top of that
type. Use this to resolve the payment method a payment references, or to discover the set of
methods available when recording payments.
Payment methods can be linked to a QuickBooks Online payment method; when they are, the response's
qb_payment_method_id holds the QuickBooks Online identifier the method is mapped to (null when it
isn't mapped).
This endpoint does not paginate: it returns every matching payment method in a single data array,
and next_page is always null. Results are scoped to your company only.
Note: this endpoint returns eventually consistent data — a create, edit, or delete can take up to 1 second to be reflected here.
Required permission: settings_permissions_payment_methods.
Request
GET /public/v1/payment-methods
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| deleted | 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.no include only |
query | string | false | no | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of payment methods | PaymentMethods |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
PaymentTerm
Get payment terms
Success scenario
GET /public/v1/payment-terms
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMjllYmNiODgtMTVjMi00NWI5LWI3ZTAtMDQzOTliNjRmYjhiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTM5IiwidHlwIjoiYWNjZXNzIn0.hc2tyr1iBV5L-j0OHkGwWWOlJ1bX0bXTroREG3V5yqw
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: bde975ca7ce16fab0bb43005971f6cee-8e468d5cf40cbb79-0
{
"data": [
{
"days": 30,
"id": "00000000-0000-0000-0000-000000000001",
"inserted_datetime": "2026-08-24T16:01:03.093446Z",
"locked": false,
"name": "Net 30",
"time_of_day": "17:00:00",
"updated_datetime": "2026-08-24T16:01:03.093446Z"
}
],
"next_page": null
}
Get the payment terms configured for your company. A payment term is the agreed timeframe a customer has to pay — for example "Net 30" means payment is due 30 days after the order date.
Use it to resolve the payment term applied to an order or invoice, or to pick a valid term id
for an integration that sets one. Each term carries:
• name — the term's display label (e.g. "Net 30"), unique within your company.
• days — a non-negative whole number of days added to compute the due date. 0 means due the
same day (immediately). The due datetime is the order's start date plus days, set to
time_of_day. The start date is normally the order date, but is the delivery date if your
company is configured to date its due dates from delivery.
• time_of_day — the local clock time the term becomes due on the due date, as HH:MM:SS on a
24-hour clock (e.g. "17:00:00" = 5 PM). It is interpreted in your company's configured time
zone, not UTC.
• locked — true marks a built-in system term whose length (days) is fixed and which cannot
be deleted; only its time_of_day is changeable. false marks a fully editable, user-created
term. Independently of locked, any term (locked or not) currently chosen as the company's
default order or default purchase payment term cannot be deleted while it holds that role — but
this endpoint does not expose which term is a default.
Returns every payment term for your company in a single response — this endpoint is not paginated,
so next_page is always null and there are no filter or page parameters. Order of the returned
terms is not guaranteed.
Note: This endpoint returns eventually consistent data — a term you just created, edited, or deleted elsewhere may take up to 1 second to appear or disappear here.
Required permission: settings_permissions_payment_terms.
Request
GET /public/v1/payment-terms
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of payment terms | PaymentTerms |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
PriceTier
Delete a price tier
Success scenario
DELETE /public/v1/price-tiers/d57eb225-d43c-4698-aa77-3ce39851b2ef
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjksImlhdCI6MTc4NzU4NzI2OSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNzEzNjc1NGEtYmQwZi00NWFkLWEzZjYtZWYzOWMwZTJlNmFiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY4LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjAyNSIsInR5cCI6ImFjY2VzcyJ9.0RItLlqtOvg3WqgvVl9Jf2kwCGsBIesXhoOsxfG3E6w
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 461b4f0a00a4c9fd47a02f1d426ac3fd-da8cac3a7c2ba7fa-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 1593386c64b273362151279a7a614bfd-ece2a17cad608e59-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Soft-deletes the tier. It stops applying to new orders immediately; order items already priced by it keep the frozen version they reference, so their prices don't change. Its removal from statewide marketplace price discovery propagates asynchronously, so a 204 confirms the tier was deleted, not that every buyer-facing surface has dropped it yet. Deleting an unknown or already-deleted tier returns 404.
Required permission: settings_permissions_price_tiers.
Request
DELETE /public/v1/price-tiers/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | Price tier ID, as returned by the list and upsert endpoints. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a price tier
Success scenario
GET /public/v1/price-tiers/843fbbae-5192-4ed6-8e8d-06b48138710d
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjgsImlhdCI6MTc4NzU4NzI2OCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNmYyMjI3MmMtOTY0Ny00NjQxLWIzZTgtZTM0MzE3YzIyMDEyIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY3LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTUzNCIsInR5cCI6ImFjY2VzcyJ9.GF2JjsTJrqdXvrEtnW5ogMAS2oD79IUWJ92MnD6_sOI
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 372bd669242034859ff4d154741d8430-276b4ae76048d610-0
{
"data": {
"conditions": {
"min_quantity": {
"quantity": "10",
"unit_type": {
"id": "00000000-0000-0000-0000-000000003831",
"name": "Unit Type 56"
}
},
"not_one_of_companies": [],
"not_one_of_company_relationship_groups": [],
"not_one_of_product_brands": [],
"not_one_of_product_categories": [],
"not_one_of_product_groups": [],
"not_one_of_product_subcategories": [],
"not_one_of_products": [],
"one_of_companies": [],
"one_of_company_relationship_groups": [
{
"id": "00000000-0000-0000-0000-000000000014",
"name": "Comp Rel Group 18"
}
],
"one_of_product_brands": [],
"one_of_product_categories": [
{
"id": "00000000-0000-0000-0000-000000000109",
"name": "Some category 262",
"official_product_category_id": "OTHER"
}
],
"one_of_product_groups": [],
"one_of_product_subcategories": [],
"one_of_products": [],
"total_thc_percentage_range": {
"max": "20.0",
"min": "5.0"
}
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1540@example.com",
"full_name": "FirstName3120 LastName3121",
"id": "00000000-0000-0000-0000-000000000607",
"inserted_datetime": "2026-08-24T16:01:08.233480Z",
"role": {
"id": "00000000-0000-0000-0000-00000000062a",
"name": "Admin 1577"
}
},
"current_version_id": "00000000-0000-0000-0000-00000000006c",
"external_name": null,
"id": "843fbbae-5192-4ed6-8e8d-06b48138710d",
"inserted_datetime": "2026-08-24T16:01:08.237136Z",
"is_flat": false,
"menu_mode": "SPECIFIC",
"menu_promo_card_background_hex": "#9F60FF",
"menu_promo_card_emoji": "🎉",
"menu_promo_card_text_hex": "#0D1D23",
"menu_promo_card_type": "TEXT",
"menu_promo_enabled": false,
"menus": [
{
"id": "00000000-0000-0000-0000-00000000002e",
"menu_id": "00000000-0000-0000-0000-00000000002e",
"menu_name": "Menu 137",
"name": "Menu 137"
}
],
"name": "VIP Flower",
"owner": null,
"percent": null,
"price": "1",
"price_or_percent": "PRICE",
"updated_datetime": "2026-08-24T16:01:08.242315Z",
"valid_from_datetime": null,
"valid_until_datetime": null
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 773bd64549abcb445476dfdd0baac242-c418d2aaacdaff65-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get a single price tier by id, with its conditions resolved into the referenced products, categories, customers, and menus. Returns 404 for an unknown or soft-deleted tier.
Required permission: settings_permissions_price_tiers.
Request
GET /public/v1/price-tiers/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | Price tier ID, as returned by the list and upsert endpoints. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single price tier | PriceTierResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get price tiers
Success scenario
GET /public/v1/price-tiers
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmY0ZTA5YmQtNmU0YS00ZGI5LWE3NzgtNGRlZWU4NjQzZmI1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTA1OCIsInR5cCI6ImFjY2VzcyJ9.tsvnJAWDbtvsrjEH7LuUc02v38Ig8bq7A12Hk3dks-8
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a0a19a00bf4130f4a34b3ce9f65620f8-1089208095168b79-0
{
"data": [
{
"conditions": {
"min_quantity": null,
"not_one_of_companies": [],
"not_one_of_company_relationship_groups": [],
"not_one_of_product_brands": [],
"not_one_of_product_categories": [],
"not_one_of_product_groups": [],
"not_one_of_product_subcategories": [],
"not_one_of_products": [],
"one_of_companies": [],
"one_of_company_relationship_groups": [],
"one_of_product_brands": [],
"one_of_product_categories": [],
"one_of_product_groups": [],
"one_of_product_subcategories": [],
"one_of_products": [],
"total_thc_percentage_range": null
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1076@example.com",
"full_name": "FirstName2192 LastName2193",
"id": "00000000-0000-0000-0000-000000000437",
"inserted_datetime": "2026-08-24T16:01:06.705706Z",
"role": {
"id": "00000000-0000-0000-0000-000000000454",
"name": "Admin 1107"
}
},
"current_version_id": "00000000-0000-0000-0000-000000000057",
"external_name": null,
"id": "4fdcfff2-447b-4398-ba91-d603d7dbc81c",
"inserted_datetime": "2025-01-03T00:00:00.000000Z",
"is_flat": false,
"menu_mode": "ALL",
"menu_promo_card_background_hex": "#9F60FF",
"menu_promo_card_emoji": "🎉",
"menu_promo_card_text_hex": "#0D1D23",
"menu_promo_card_type": "TEXT",
"menu_promo_enabled": false,
"menus": [],
"name": "T3",
"owner": null,
"percent": null,
"price": "1",
"price_or_percent": "PRICE",
"updated_datetime": "2026-08-24T16:01:06.712438Z",
"valid_from_datetime": null,
"valid_until_datetime": null
},
{
"conditions": {
"min_quantity": null,
"not_one_of_companies": [],
"not_one_of_company_relationship_groups": [],
"not_one_of_product_brands": [],
"not_one_of_product_categories": [],
"not_one_of_product_groups": [],
"not_one_of_product_subcategories": [],
"not_one_of_products": [],
"one_of_companies": [],
"one_of_company_relationship_groups": [],
"one_of_product_brands": [],
"one_of_product_categories": [],
"one_of_product_groups": [],
"one_of_product_subcategories": [],
"one_of_products": [],
"total_thc_percentage_range": null
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1073@example.com",
"full_name": "FirstName2186 LastName2187",
"id": "00000000-0000-0000-0000-000000000434",
"inserted_datetime": "2026-08-24T16:01:06.692287Z",
"role": {
"id": "00000000-0000-0000-0000-000000000451",
"name": "Admin 1104"
}
},
"current_version_id": "00000000-0000-0000-0000-000000000056",
"external_name": null,
"id": "9df5711e-dc09-4411-a362-673210a9d15e",
"inserted_datetime": "2025-01-02T00:00:00.000000Z",
"is_flat": false,
"menu_mode": "ALL",
"menu_promo_card_background_hex": "#9F60FF",
"menu_promo_card_emoji": "🎉",
"menu_promo_card_text_hex": "#0D1D23",
"menu_promo_card_type": "TEXT",
"menu_promo_enabled": false,
"menus": [],
"name": "T2",
"owner": null,
"percent": null,
"price": "1",
"price_or_percent": "PRICE",
"updated_datetime": "2026-08-24T16:01:06.699224Z",
"valid_from_datetime": null,
"valid_until_datetime": null
},
{
"conditions": {
"min_quantity": null,
"not_one_of_companies": [],
"not_one_of_company_relationship_groups": [],
"not_one_of_product_brands": [],
"not_one_of_product_categories": [],
"not_one_of_product_groups": [],
"not_one_of_product_subcategories": [],
"not_one_of_products": [],
"one_of_companies": [],
"one_of_company_relationship_groups": [],
"one_of_product_brands": [],
"one_of_product_categories": [],
"one_of_product_groups": [],
"one_of_product_subcategories": [],
"one_of_products": [],
"total_thc_percentage_range": null
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1066@example.com",
"full_name": "FirstName2172 LastName2173",
"id": "00000000-0000-0000-0000-00000000042d",
"inserted_datetime": "2026-08-24T16:01:06.666266Z",
"role": {
"id": "00000000-0000-0000-0000-00000000044a",
"name": "Admin 1097"
}
},
"current_version_id": "00000000-0000-0000-0000-000000000055",
"external_name": null,
"id": "4fc1b400-615a-48bb-b71a-cacb151ca39e",
"inserted_datetime": "2025-01-01T00:00:00.000000Z",
"is_flat": false,
"menu_mode": "ALL",
"menu_promo_card_background_hex": "#9F60FF",
"menu_promo_card_emoji": "🎉",
"menu_promo_card_text_hex": "#0D1D23",
"menu_promo_card_type": "TEXT",
"menu_promo_enabled": false,
"menus": [],
"name": "T1",
"owner": null,
"percent": null,
"price": "1",
"price_or_percent": "PRICE",
"updated_datetime": "2026-08-24T16:01:06.686042Z",
"valid_from_datetime": null,
"valid_until_datetime": null
}
],
"next_page": null
}
Error scenario: invalid page param
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 751bb6f8244681f1bca52997400b61c0-bd1a1bfd41d5b405-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
List the company's price tiers, newest first (by creation time). A price tier lowers the price of a matching sales order item — it applies to an order item only when every one of the tier's populated conditions is met. Soft-deleted tiers are excluded. Tiers outside their active window are still listed; the window only governs whether a tier applies to orders, not whether it appears here.
Filter with company_relationship_id (a customer condition), product_ids (a product
condition), or search (substring on the internal name). When several filters are supplied a
tier must satisfy all of them (AND).
Results are paginated. Walk pages with page[number]; the response's next_page holds the URL
of the following page, or null on the last page.
Required permission: settings_permissions_price_tiers.
Request
GET /public/v1/price-tiers
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| company_relationship_id | 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. | query | string | false | ||
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | Page to return via page[number] (1-based, must be greater than 0). Defaults to the first page. |
query | number | false | ?page[number]=1 | |
| product_ids | 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. |
query | array | false | ?product_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| search | Case-insensitive substring match on the tier's internal name. Does not search external_name. | query | string | false | ||
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of price tiers | PriceTiers |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a price tier
Success scenario
POST /public/v1/price-tiers
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiN2QyZDQzYjUtMzY4ZC00NDdiLTg2MzAtYjQ5NDY4NmQxMDBmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTMzMSIsInR5cCI6ImFjY2VzcyJ9.wYB2aZtSa0kFOGink3ukBLZnoLMXS2BQN-6gRBuRyas
{
"conditions": {
"min_quantity": {
"quantity": 10,
"unit_type_id": "00000000-0000-0000-0000-00000000325a"
},
"one_of_product_category_ids": [
"00000000-0000-0000-0000-0000000000d5"
],
"total_thc_percentage_range": {
"max": 20.0,
"min": 5.0
}
},
"external_name": "VIP Discount",
"menu_ids": [
"00000000-0000-0000-0000-000000000025"
],
"menu_mode": "SPECIFIC",
"name": "VIP Flower",
"percent": 15,
"price": "5",
"price_or_percent": "PERCENT"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ef02d5978401c222dd64fa2cdfde0646-5918994aa45d96b3-0
{
"data": {
"conditions": {
"min_quantity": {
"quantity": "10",
"unit_type": {
"id": "00000000-0000-0000-0000-00000000325a",
"name": "Unit Type 49"
}
},
"not_one_of_companies": [],
"not_one_of_company_relationship_groups": [],
"not_one_of_product_brands": [],
"not_one_of_product_categories": [],
"not_one_of_product_groups": [],
"not_one_of_product_subcategories": [],
"not_one_of_products": [],
"one_of_companies": [],
"one_of_company_relationship_groups": [],
"one_of_product_brands": [],
"one_of_product_categories": [
{
"id": "00000000-0000-0000-0000-0000000000d5",
"name": "Some category 210",
"official_product_category_id": "OTHER"
}
],
"one_of_product_groups": [],
"one_of_product_subcategories": [],
"one_of_products": [],
"total_thc_percentage_range": {
"max": "20.0",
"min": "5.0"
}
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1329@example.com",
"full_name": "FirstName2698 LastName2699",
"id": "00000000-0000-0000-0000-000000000533",
"inserted_datetime": "2026-08-24T16:01:07.560488Z",
"role": {
"id": "00000000-0000-0000-0000-000000000557",
"name": "Admin 1366"
}
},
"current_version_id": "00000000-0000-0000-0000-000000000062",
"external_name": "VIP Discount",
"id": "24d7315b-3f63-48b9-aaff-ae4d31866c29",
"inserted_datetime": "2026-08-24T16:01:07.737849Z",
"is_flat": false,
"menu_mode": "SPECIFIC",
"menu_promo_card_background_hex": "#9F60FF",
"menu_promo_card_emoji": null,
"menu_promo_card_text_hex": "#0D1D23",
"menu_promo_card_type": "TEXT",
"menu_promo_enabled": false,
"menus": [
{
"id": "00000000-0000-0000-0000-000000000025",
"menu_id": "00000000-0000-0000-0000-000000000025",
"menu_name": "Menu 110",
"name": "Menu 110"
}
],
"name": "VIP Flower",
"owner": null,
"percent": 15,
"price": null,
"price_or_percent": "PERCENT",
"updated_datetime": "2026-08-24T16:01:07.741492Z",
"valid_from_datetime": null,
"valid_until_datetime": null
}
}
Error scenario: no conditions provided
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: eaf6de77ad66ad43a2bc30aaffb02c5f-f25f89e581a484a3-0
{
"errors": [
{
"context": {},
"message": "Please select at least 1 filter condition",
"pointer": [
"conditions"
],
"section": "body"
}
]
}
Create a price tier when id is absent, update it in place when present. Every write records a
new immutable version and points the tier's current_version_id at it, so order items priced
by an earlier version keep their frozen pricing (see price_tier_version on the
sales order item).
Updates are sparse: only the fields you send change, and anything you omit is left as-is. This
applies to conditions too — send only the conditions you want to change, and send an empty
array (or null) for a one_of_*/not_one_of_* list to clear that one. Omitting menu_ids leaves the
tier's current menus untouched; send the full list to replace them. When you send the
min_quantity or total_thc_percentage_range object, include every sub-field — a present
sub-field may be null, but a missing one is rejected. Send the object as null to clear it.
conditions decides whether the tier can apply to a sales order item — every populated
condition must be met together (AND), or the tier is not applicable. Within a single "one of"
list any listed match qualifies; a "not one of" list disqualifies any listed match;
min_quantity is compared against the combined quantity of the order's matching items; and
total_thc_percentage_range applies only where selling by potency is enabled. A create must
resolve to at least one condition, otherwise it is rejected with a 400. Creating a tier also
requires your company to be set up for price tiers; if it is not, the create fails with a 400
and a message to contact Distru support.
Saving a tier also refreshes buyer-facing and statewide marketplace price discovery, which happens asynchronously — a 200/201 reflects the saved tier itself, not yet its propagation to every menu or marketplace surface that shows it. When a promo-enabled tier newly becomes eligible for a menu, that menu gets a fresh promo entry.
Promo card fields default sensibly on create: omit menu_promo_card_type and it becomes TEXT,
and a TEXT card fills menu_promo_card_background_hex and menu_promo_card_text_hex with
default colors when you omit them. Send menu_promo_card_type: "IMAGE" for an image card, where
the hex colors are optional. menu_promo_enabled (default false) controls whether the card shows
on menus.
Required permission: settings_permissions_price_tiers.
Request
POST /public/v1/price-tiers
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| external_name | Buyer-facing name shown on menus and marketplace surfaces, up to 255 characters. When omitted or null, buyer-facing surfaces fall back to name. |
body | string | false | ||
| id | 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. | body | string | false | ||
| is_flat | 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. |
body | boolean | false | ||
| menu_ids | 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. |
body | array | false | ||
| menu_mode | 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.ALL NONE SPECIFIC |
body | string | true | ||
| menu_promo_card_background_hex | 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. |
body | string | false | ||
| menu_promo_card_emoji | Emoji shown on the promo card. Optional; null for none. | body | string | false | ||
| menu_promo_card_text_hex | 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. |
body | string | false | ||
| menu_promo_card_type | 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.TEXT IMAGE |
body | string | false | ||
| menu_promo_enabled | 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. |
body | boolean | false | ||
| min_quantity | 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. | body | object | false | ||
| name | Internal name of the tier, up to 255 characters. Required. | body | string | true | ||
| not_one_of_company_ids | order_item.order.company must not be one of these | body | array(string) | false | ||
| not_one_of_company_relationship_group_ids | order_item.order.company.group must not be one of these | body | array(string) | false | ||
| not_one_of_product_brand_ids | order_item.product.brand must not be one of these | body | array(string) | false | ||
| not_one_of_product_category_ids | order_item.product.category must not be one of these | body | array(string) | false | ||
| not_one_of_product_group_ids | order_item.product.group must not be one of these | body | array(string) | false | ||
| not_one_of_product_ids | order_item.product must not be one of these | body | array(string) | false | ||
| not_one_of_product_subcategory_ids | order_item.product.subcategory must not be one of these | body | array(string) | false | ||
| one_of_company_ids | order_item.order.company must be one of these | body | array(string) | false | ||
| one_of_company_relationship_group_ids | order_item.order.company.group must be one of these | body | array(string) | false | ||
| one_of_product_brand_ids | order_item.product.brand must be one of these | body | array(string) | false | ||
| one_of_product_category_ids | order_item.product.category must be one of these | body | array(string) | false | ||
| one_of_product_group_ids | order_item.product.group must be one of these | body | array(string) | false | ||
| one_of_product_ids | order_item.product must be one of these | body | array(string) | false | ||
| one_of_product_subcategory_ids | order_item.product.subcategory must be one of these | body | array(string) | false | ||
| owner_id | 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. | body | string | false | ||
| percent | Discount percentage off the base price, an integer 0-100. Required when price_or_percent is PERCENT; switching to PRICE clears it automatically. |
body | integer | false | ||
| price | 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. |
body | string | false | ||
| price_or_percent | 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 PERCENT |
body | string | true | ||
| total_thc_percentage_range | 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. | body | object | false | ||
| valid_from_datetime | 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. | body | string | false | ||
| valid_until_datetime | ISO8601 UTC end of the active window (e.g. "2026-12-31T23:59:59Z"). Null or omitted means no end bound (never expires). | body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated price tier | PriceTierResponse |
| 201 | The created price tier | PriceTierResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Product
Delete a product
Success scenario
DELETE /public/v1/products/72b4c822-023e-4b47-bcc1-5d9a0656c90a
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzMsImlhdCI6MTc4NzU4NzI3MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmE0NTNlYjgtNjRkZS00NTNlLWI0NGEtOGE3ZWYxYTBjYzk1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzE0MiIsInR5cCI6ImFjY2VzcyJ9.prQ9oJP7X1X_OLC9F0AlSUp3TwgJHRCOSQn0mm-JP5o
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 5448ba9743f5da802811d6c7d10286db-5a5edcd33594b7bb-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ba814631d8868f824545ba38e52cf1fa-ff618aa77fb9cfae-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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).
A 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.
The delete cascades, all in one atomic call: the product's batches are soft-deleted, its own bill of materials 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.
Required permission: products_permissions_delete (plus access to the product under team restrictions).
Request
DELETE /public/v1/products/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a product
Success scenario
GET /public/v1/products/72b4c822-023e-4b47-bcc1-5d9a0656c90a
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzMsImlhdCI6MTc4NzU4NzI3MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmE0NTNlYjgtNjRkZS00NTNlLWI0NGEtOGE3ZWYxYTBjYzk1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzE0MiIsInR5cCI6ImFjY2VzcyJ9.prQ9oJP7X1X_OLC9F0AlSUp3TwgJHRCOSQn0mm-JP5o
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c6d9af030ba4abc508627f1c003ec325-1c2320f89e7099f5-0
{
"data": {
"bill_of_materials": null,
"brand": null,
"category": {
"id": "00000000-0000-0000-0000-0000000002b4",
"name": "Some category 689",
"official_product_category_id": "OTHER"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-3127@example.com",
"full_name": "FirstName6352 LastName6353",
"id": "00000000-0000-0000-0000-000000000c47",
"inserted_datetime": "2026-08-24T16:01:13.744813Z",
"role": {
"id": "00000000-0000-0000-0000-000000000c8a",
"name": "Admin 3209"
}
},
"custom_data": [],
"deleted_at": null,
"description": null,
"description_markdown": null,
"external_name": null,
"gross_weight": null,
"gross_weight_unit_type": null,
"id": "72b4c822-023e-4b47-bcc1-5d9a0656c90a",
"images": [
{
"id": "00000000-0000-0000-0000-00000000000d",
"name": "Image Name 163",
"rank": 0,
"url": "https://google.com/original-8.jpg"
}
],
"inserted_datetime": "2026-08-24T16:01:13.754681Z",
"inventory_tracking_method": "PRODUCT",
"is_active": true,
"is_featured": false,
"leaflink_product_id": 777,
"menu_visibility": "INCLUDE_IN_SELECT",
"menus": [
{
"id": "00000000-0000-0000-0000-000000000043",
"menu_id": "00000000-0000-0000-0000-000000000043",
"menu_name": "Menu 1",
"name": "Menu 1"
}
],
"msrp": null,
"name": "Test Product",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-3127@example.com",
"full_name": "FirstName6352 LastName6353",
"id": "00000000-0000-0000-0000-000000000c47",
"inserted_datetime": "2026-08-24T16:01:13.744813Z",
"role": {
"id": "00000000-0000-0000-0000-000000000c8a",
"name": "Admin 3209"
}
},
"product_group": {
"id": "00000000-0000-0000-0000-00000000029e",
"name": "Product Group 667"
},
"quantity_active": "0",
"quantity_active_by_location": [],
"quantity_available": "0",
"quantity_available_threshold_max": null,
"quantity_available_threshold_min": null,
"quantity_reserved": "0",
"sku": "SKU001",
"strain": null,
"subcategory": {
"id": "00000000-0000-0000-0000-00000000029f",
"name": "Some subcategory 669"
},
"tags": [
{
"id": "00000000-0000-0000-0000-000000000020",
"name": "Tag 1"
}
],
"total_cannabinoid_unit": null,
"total_cbd": null,
"total_thc": null,
"treez_wholesale_price": "9.99",
"unit_cost": null,
"unit_net_weight": null,
"unit_net_weight_serving_size_unit_type": null,
"unit_price": "1",
"unit_serving_size": null,
"unit_type": {
"id": "00000000-0000-0000-0000-000000006d99",
"name": "Gram"
},
"units_per_case": null,
"upc": null,
"updated_datetime": "2026-08-24T16:01:13.754681Z",
"vendor": {
"id": "00000000-0000-0000-0000-000000000464",
"name": "Company 2220",
"updated_datetime": "2026-08-24T16:01:13.751659Z"
},
"wholesale_unit_price": null
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 3fb049181f7b1b9cc2bd48b689665f1d-2bf5b7b0ca7fbc16-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get a single product by ID. Unlike the list endpoint, the response always
includes the product's bill_of_materials (or null if it has none).
Returns 404 if no product with that ID exists in your company, or if the authenticated user cannot see it under their team restrictions — the two cases are indistinguishable from the response.
Required permission: products_permissions_view.
Request
GET /public/v1/products/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | Product ID. | path | string | true | ||
| include_batches_with_active_quantity_by_location | 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. |
query | boolean | false | false | ?include_batches_with_active_quantity_by_location=true |
| include_packages_with_active_quantity_by_location | 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. |
query | boolean | false | false | ?include_packages_with_active_quantity_by_location=true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single product | ProductResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get products
Success scenario
GET /public/v1/products
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjgsImlhdCI6MTc4NzU4NzI2OCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNjU5YTlkZTItNTdlZC00YWEwLWJmNDUtYjcyZDQ2NWQ1ZWEyIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY3LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTYxNCIsInR5cCI6ImFjY2VzcyJ9.6X3GGIYLCdPOeiDgvgr6JueZN0NZ3ltdn-T3gdSqD1o
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ca10a4e37cc2f670cf3bd72a215d1579-cd387c9fb62c3ede-0
{
"data": [
{
"brand": {
"id": "00000000-0000-0000-0000-0000000001e4",
"name": "Company 1208",
"updated_datetime": "2030-11-01T00:00:00.000000Z"
},
"category": {
"id": "00000000-0000-0000-0000-00000000011d",
"name": "Some category 282",
"official_product_category_id": "OTHER"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "product-owner@example.com",
"full_name": "FirstName3352 LastName3353",
"id": "00000000-0000-0000-0000-00000000067a",
"inserted_datetime": "2026-08-24T16:01:08.513296Z",
"role": {
"id": "00000000-0000-0000-0000-00000000069c",
"name": "Admin 1691"
}
},
"custom_data": [
{
"id": 56,
"name": "Custom Field 35",
"value": "Custom Field Value 1"
}
],
"deleted_at": null,
"description": "test",
"description_markdown": "# test",
"external_name": "External Name",
"gross_weight": null,
"gross_weight_unit_type": null,
"id": "10ed88c2-a03f-4a06-a334-b8b9c4d1c611",
"images": [
{
"id": "00000000-0000-0000-0000-00000000000b",
"name": "Image Name 99",
"rank": 0,
"url": "https://google.com/original-6.jpg"
},
{
"id": "00000000-0000-0000-0000-00000000000c",
"name": "Image Name 100",
"rank": 1,
"url": "https://google.com/original-7.jpg"
}
],
"inserted_datetime": "2026-08-24T16:01:08.524142Z",
"inventory_tracking_method": "BATCH",
"is_active": true,
"is_featured": true,
"leaflink_product_id": 555,
"menu_visibility": "INCLUDE_IN_ALL",
"menus": [
{
"id": "00000000-0000-0000-0000-000000000036",
"menu_id": "00000000-0000-0000-0000-000000000036",
"menu_name": "Menu 1",
"name": "Menu 1"
}
],
"msrp": null,
"name": "Product 720",
"owner": {
"banned": false,
"deleted_at": null,
"email": "product-owner@example.com",
"full_name": "FirstName3352 LastName3353",
"id": "00000000-0000-0000-0000-00000000067a",
"inserted_datetime": "2026-08-24T16:01:08.513296Z",
"role": {
"id": "00000000-0000-0000-0000-00000000069c",
"name": "Admin 1691"
}
},
"product_group": {
"id": "00000000-0000-0000-0000-00000000010a",
"name": "Product Group 263"
},
"quantity_active": "0",
"quantity_active_by_location": [],
"quantity_available": "0",
"quantity_available_threshold_max": "50",
"quantity_available_threshold_min": "5",
"quantity_reserved": "0",
"sku": "sku 721",
"strain": {
"id": "00000000-0000-0000-0000-000000000034",
"name": "Strain 47",
"strain_type": "INDICA"
},
"subcategory": {
"id": "00000000-0000-0000-0000-00000000010b",
"name": "Some subcategory 265"
},
"tags": [
{
"id": "00000000-0000-0000-0000-000000000016",
"name": "Tag 1"
}
],
"total_cannabinoid_unit": "PERCENT",
"total_cbd": "3",
"total_thc": "12",
"treez_wholesale_price": "12.34",
"unit_cost": null,
"unit_net_weight": "20",
"unit_net_weight_serving_size_unit_type": {
"id": "00000000-0000-0000-0000-000000003a22",
"name": "Ounce"
},
"unit_price": "1",
"unit_serving_size": "10",
"unit_type": {
"id": "00000000-0000-0000-0000-000000003a20",
"name": "Gram"
},
"units_per_case": null,
"upc": "036000291452",
"updated_datetime": "2023-11-01T00:00:00.000000Z",
"vendor": {
"id": "00000000-0000-0000-0000-0000000001ec",
"name": "Company 1222",
"updated_datetime": "2030-11-03T00:00:00.000000Z"
},
"wholesale_unit_price": 90.5
},
{
"brand": {
"id": "00000000-0000-0000-0000-0000000001e8",
"name": "Company 1217",
"updated_datetime": "2030-11-02T00:00:00.000000Z"
},
"category": {
"id": "00000000-0000-0000-0000-000000000124",
"name": "Some category 289",
"official_product_category_id": "OTHER"
},
"creator": null,
"custom_data": [
{
"id": 56,
"name": "Custom Field 35",
"value": null
}
],
"deleted_at": null,
"description": null,
"description_markdown": null,
"external_name": null,
"gross_weight": null,
"gross_weight_unit_type": null,
"id": "b9818dd7-ce7d-40b4-b5ef-d9b504ef588c",
"images": [],
"inserted_datetime": "2026-08-24T16:01:08.580130Z",
"inventory_tracking_method": "PACKAGE",
"is_active": false,
"is_featured": false,
"leaflink_product_id": null,
"menu_visibility": "DO_NOT_INCLUDE",
"menus": [],
"msrp": "100",
"name": "Product 739",
"owner": {
"banned": false,
"deleted_at": null,
"email": "product-owner@example.com",
"full_name": "FirstName3352 LastName3353",
"id": "00000000-0000-0000-0000-00000000067a",
"inserted_datetime": "2026-08-24T16:01:08.513296Z",
"role": {
"id": "00000000-0000-0000-0000-00000000069c",
"name": "Admin 1691"
}
},
"product_group": {
"id": "00000000-0000-0000-0000-000000000111",
"name": "Product Group 269"
},
"quantity_active": "0",
"quantity_active_by_location": [],
"quantity_available": "0",
"quantity_available_threshold_max": null,
"quantity_available_threshold_min": null,
"quantity_reserved": "0",
"sku": "sku 740",
"strain": null,
"subcategory": {
"id": "00000000-0000-0000-0000-000000000112",
"name": "Some subcategory 272"
},
"tags": [],
"total_cannabinoid_unit": null,
"total_cbd": null,
"total_thc": null,
"treez_wholesale_price": null,
"unit_cost": null,
"unit_net_weight": null,
"unit_net_weight_serving_size_unit_type": null,
"unit_price": "1",
"unit_serving_size": null,
"unit_type": {
"id": "00000000-0000-0000-0000-000000003a1e",
"name": "Pound"
},
"units_per_case": null,
"upc": null,
"updated_datetime": "2023-11-02T00:00:00.000000Z",
"vendor": {
"id": "00000000-0000-0000-0000-0000000001f0",
"name": "Company 1228",
"updated_datetime": "2030-11-04T00:00:00.000000Z"
},
"wholesale_unit_price": null
}
],
"next_page": null
}
Error scenario: invalid ids filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 495fd98059fe1116961f624ba5d6696c-14279800d981299a-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"ids"
],
"section": "query"
}
]
}
Returns a paginated list of products, oldest first (ascending by creation
time). Soft-deleted products are excluded unless you opt into them with the
deleted filter. The response is a {data, next_page} envelope; follow
next_page (null on the last page) to walk every page.
This endpoint is eventually consistent: a create or update made through the API can take up to about one second to appear or change here, so a product you just wrote may briefly be missing or stale in the list.
Required permission: products_permissions_view. Results are additionally
limited to the products the authenticated user can see under their team
restrictions, so two API keys at the same company can return different sets.
Request
GET /public/v1/products
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| brand_ids | 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. |
query | array | false | ||
| category_ids | 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. |
query | array | false | ||
| custom_data | 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. |
query | object | false | ?custom_data[101]=Blue&custom_data[102]=Wholesale | |
| deleted | 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.no include only |
query | string | false | no | |
| has_quantity_active | Restrict to products that currently hold active inventory quantity (true) or none (false). Omit to match products regardless of active quantity. |
query | boolean | false | ||
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| include_batches_with_active_quantity_by_location | 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. |
query | boolean | false | false | ?include_batches_with_active_quantity_by_location=true |
| include_bill_of_materials | 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. |
query | boolean | false | false | |
| include_packages_with_active_quantity_by_location | 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. |
query | boolean | false | false | ?include_packages_with_active_quantity_by_location=true |
| inserted_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z, | |
| inventory_tracking_method | Restrict to products with this inventory tracking method (the same value returned as each product's inventory_tracking_method). SCREAMING_CASE.PACKAGE BATCH PRODUCT |
query | string | false | ||
| is_active | Restrict to active (true) or inactive (false) products. Omit to match products regardless of active state. |
query | boolean | false | ||
| is_featured | Restrict to featured (true) or non-featured (false) products. Omit to match products regardless of featured state. |
query | boolean | false | ||
| leaflink_product_ids | 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. |
query | array | false | ||
| menu_id | 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. |
query | string | false | ?menu_id=550e8400-e29b-41d4-a716-446655440000,6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| menu_ids | 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. |
query | array | false | ?menu_ids[]=550e8400-e29b-41d4-a716-446655440000&menu_ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| menu_name | Case-insensitive substring match on the name of a menu the product belongs to. When combined with menu_ids, both must hold (AND). |
query | string | false | ||
| menu_visibility | Restrict to products with this menu visibility (the same value returned as each product's menu_visibility). SCREAMING_CASE.DO_NOT_INCLUDE INCLUDE_IN_ALL INCLUDE_IN_SELECT |
query | string | false | ||
| name | Case-insensitive substring match on the product's name. Omit to match products of any name. | query | string | false | ||
| names | Restrict to products whose name exactly matches (case-insensitive) any value in the list. Repeat the bracketed key once per value. | query | array | false | ?names[]=King Size Pre-rolls&names[]=Live Resin | |
| owner_ids | 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. |
query | array | false | ||
| page | Page to fetch, 1-indexed. Defaults to page 1 when omitted. page[number] must be greater than 0. |
query | number | false | ?page[number]=1 | |
| product_group_ids | 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. |
query | array | false | ||
| sku | Case-insensitive substring match on the product's SKU. Omit to match products of any SKU. | query | string | false | ||
| skus | Restrict to products whose SKU exactly matches (case-insensitive) any value in the list. Repeat the bracketed key once per value. | query | array | false | ?skus[]=SKU123&skus[]=SKU456 | |
| strain_ids | 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. |
query | array | false | ||
| strain_types | Restrict to products whose strain is of any of these types. Repeat the bracketed key once per value. SCREAMING_CASE. INDICA INDICA_DOMINANT SATIVA SATIVA_DOMINANT HYBRID HIGH_CBD |
query | array | false | ||
| subcategory_ids | 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. |
query | array | false | ||
| tag_ids | 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. |
query | array | false | ||
| unit_type_ids | 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. |
query | array | false | ||
| upc | Case-insensitive substring match on the product's UPC. Omit to match products of any UPC. | query | string | false | ||
| upcs | Restrict to products whose UPC exactly matches (case-insensitive) any value in the list. Repeat the bracketed key once per value. | query | array | false | ?upcs[]=123456789012&upcs[]=987654321098 | |
| updated_datetime | 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. |
query | string | false | ,2022-07-10T00:00:00Z | |
| vendor_ids | 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. |
query | array | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of products | Products |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a product
Success scenario
POST /public/v1/products
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzcsImlhdCI6MTc4NzU4NzI3NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYWIwOTJkMGMtZTFmMS00ZWZmLWFmZDUtOWE1YjUzOGU2ZGE5IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3Mjc2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6Mzc1MSIsInR5cCI6ImFjY2VzcyJ9.8OVTW-E4GYiZDALklXRlh3IRD28Wv70zx46Z-QWwFLA
{
"brand_id": "00000000-0000-0000-0000-0000000005b6",
"category_id": "00000000-0000-0000-0000-0000000003c1",
"description": "My Product Description",
"external_name": "External Name",
"gross_weight": "9.9",
"gross_weight_unit_type_id": "00000000-0000-0000-0000-00000000842a",
"group_id": "00000000-0000-0000-0000-0000000003a8",
"id": "377549fc-bb06-4282-b59a-7532c1c50352",
"inventory_tracking_method": "PACKAGE",
"is_featured": true,
"is_inactive": true,
"menu_visibility": "INCLUDE_IN_ALL",
"menus": [
"00000000-0000-0000-0000-000000000053"
],
"msrp": "100.5",
"name": "Updated Name",
"owner_id": "00000000-0000-0000-0000-000000000ed5",
"quantity_available_threshold_max": "10.5",
"quantity_available_threshold_min": "5.5",
"sku": "45678",
"strain_id": "00000000-0000-0000-0000-000000000046",
"subcategory_id": "00000000-0000-0000-0000-0000000003a8",
"tags": [
"00000000-0000-0000-0000-00000000002c"
],
"total_cannabinoid_unit": "PERCENT",
"total_cbd": "5.2",
"total_thc": "10.4",
"unit_cost": "50.4",
"unit_net_weight": "3.1",
"unit_net_weight_and_serving_size_unit_type_id": "00000000-0000-0000-0000-00000000842c",
"unit_price": "200",
"unit_serving_size": "2.2",
"unit_type_id": "00000000-0000-0000-0000-000000008433",
"units_per_case": "0.2",
"upc": "036000291453",
"vendor_id": "00000000-0000-0000-0000-0000000005b0",
"wholesale_unit_price": "90.50"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 8005c11ba1c1575366d43e0c8885f18f-7a290b1582739322-0
{
"data": {
"brand": {
"id": "00000000-0000-0000-0000-0000000005b6",
"name": "Company 2687",
"updated_datetime": "2026-08-24T16:01:17.316968Z"
},
"category": {
"id": "00000000-0000-0000-0000-0000000003c1",
"name": "Some category 958",
"official_product_category_id": "OTHER"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-3728@example.com",
"full_name": "FirstName7562 LastName7563",
"id": "00000000-0000-0000-0000-000000000ea7",
"inserted_datetime": "2026-08-24T16:01:17.066387Z",
"role": {
"id": "00000000-0000-0000-0000-000000000ee5",
"name": "Admin 3812"
}
},
"custom_data": [],
"deleted_at": null,
"description": "My Product Description",
"description_markdown": "My Product Description",
"external_name": "External Name",
"gross_weight": "9.9",
"gross_weight_unit_type": {
"id": "00000000-0000-0000-0000-00000000842a",
"name": "Gram"
},
"id": "377549fc-bb06-4282-b59a-7532c1c50352",
"images": [
{
"id": "00000000-0000-0000-0000-00000000000e",
"name": "Image Name 180",
"rank": 0,
"url": "https://google.com/original-9.jpg"
},
{
"id": "00000000-0000-0000-0000-00000000000f",
"name": "Image Name 181",
"rank": 1,
"url": "https://google.com/original-10.jpg"
}
],
"inserted_datetime": "2026-08-24T16:01:17.095425Z",
"inventory_tracking_method": "PACKAGE",
"is_active": false,
"is_featured": true,
"leaflink_product_id": null,
"menu_visibility": "INCLUDE_IN_ALL",
"menus": [
{
"id": "00000000-0000-0000-0000-000000000053",
"menu_id": "00000000-0000-0000-0000-000000000053",
"menu_name": "Menu 248",
"name": "Menu 248"
}
],
"msrp": "100.5",
"name": "Updated Name",
"owner": {
"banned": false,
"deleted_at": null,
"email": "user2@a.com",
"full_name": "FirstName7654 LastName7655",
"id": "00000000-0000-0000-0000-000000000ed5",
"inserted_datetime": "2026-08-24T16:01:17.352711Z",
"role": {
"id": "00000000-0000-0000-0000-000000000f10",
"name": "Admin 3855"
}
},
"product_group": {
"id": "00000000-0000-0000-0000-0000000003a8",
"name": "Product Group 933"
},
"quantity_active": "0",
"quantity_active_by_location": [],
"quantity_available": "0",
"quantity_available_threshold_max": "10.5",
"quantity_available_threshold_min": "5.5",
"quantity_reserved": "0",
"sku": "45678",
"strain": {
"id": "00000000-0000-0000-0000-000000000046",
"name": "Strain 65",
"strain_type": null
},
"subcategory": {
"id": "00000000-0000-0000-0000-0000000003a8",
"name": "Some subcategory 933"
},
"tags": [
{
"id": "00000000-0000-0000-0000-00000000002c",
"name": "Some tag 40"
}
],
"total_cannabinoid_unit": "PERCENT",
"total_cbd": "5.2",
"total_thc": "10.4",
"treez_wholesale_price": null,
"unit_cost": "50.4",
"unit_net_weight": "3.1",
"unit_net_weight_serving_size_unit_type": {
"id": "00000000-0000-0000-0000-00000000842c",
"name": "Ounce"
},
"unit_price": "200",
"unit_serving_size": "2.2",
"unit_type": {
"id": "00000000-0000-0000-0000-000000008433",
"name": "Unit"
},
"units_per_case": "0.2",
"upc": "036000291453",
"updated_datetime": "2026-08-24T16:01:17.380323Z",
"vendor": {
"id": "00000000-0000-0000-0000-0000000005b0",
"name": "Company 2681",
"updated_datetime": "2026-08-24T16:01:17.292269Z"
},
"wholesale_unit_price": 90.5
}
}
Error scenario: invalid inventory_tracking_method
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: e87f96d4989733496e097d3895c572c7-746b153000642d6f-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"inventory_tracking_method"
],
"section": "body"
}
]
}
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.
Updates 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.
Some 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.
This 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 is readable in the response but is NOT settable here — manage it through its own endpoints.
Required 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.
Request
POST /public/v1/products
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| brand_id | ID of the company relationship for the brand company associated with this product. This is the company-relationship ID, not the raw company ID. | body | string | false | ||
| category_id | 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. |
body | string | false | ||
| custom_data | 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. |
body | object | false | {"101":"Some text value","102":"2026-08-18T00:00:00.000-07:00","103":["Option A","Option B"]} | |
| 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. |
body | string | false | A pack of 5 pre-rolls | |
| description_markdown | 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. |
body | string | false | A pack of 5 pre-rolls | |
| external_name | 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. |
body | string | false | ||
| gross_weight | 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). |
body | number | false | ||
| gross_weight_unit_type_id | 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). |
body | string | false | ||
| group_id | ID of the product's group. | body | string | false | ||
| id | 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. | body | string | false | ||
| inventory_tracking_method | 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 BATCH PRODUCT |
body | string | false | PACKAGE | |
| is_featured | Whether the product is featured. Featured products are shown at the top of menus. Defaults to false when omitted on create. | body | boolean | false | ||
| is_inactive | 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. | body | boolean | false | ||
| leaflink_product_id | 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. | body | integer | false | ||
| menu_visibility | 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:
menus is provided — sending menus without menu_visibility is rejected.DO_NOT_INCLUDE INCLUDE_IN_ALL INCLUDE_IN_SELECT |
body | string | false | ||
| menus | 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. |
body | array | false | ["0ef8347c-b714-4cd9-ba0e-872488bc9244", "daa0294c-833c-42bd-a133-b4c9e7f64017"] | |
| msrp | 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 | body | number | false | ||
| name | Product name. Required on create and must be unique within your company. Cannot contain the characters :, [, or ], or other Metrc-disallowed special characters. |
body | string | false | King Size Pre-rolls | |
| owner_id | 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. | body | string | false | ||
| quantity_available_threshold_max | 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. |
body | number | false | ||
| quantity_available_threshold_min | 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. |
body | number | false | ||
| sku | Stock Keeping Unit (SKU). Required on create and must be unique within your company. | body | string | false | SKU123 | |
| strain_id | ID of the strain associated with the product. | body | string | false | ||
| subcategory_id | ID of the product's subcategory. When provided it must be a child of the category given in category_id. |
body | string | false | ||
| tags | 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. |
body | array | false | ["0ef8347c-b714-4cd9-ba0e-872488bc9244", "daa0294c-833c-42bd-a133-b4c9e7f64017"] | |
| total_cannabinoid_unit | Unit for this product's THC/CBD content. One of PERCENT or MG (SCREAMING_CASE). Required whenever total_thc or total_cbd is provided. |
body | string | false | ||
| total_cbd | 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. |
body | string | false | ||
| total_thc | 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. |
body | string | false | ||
| treez_wholesale_price | 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. | body | number | false | ||
| unit_cost | Cost of the product per unit, as a decimal. When provided must be non-negative (>= 0). | body | number | false | ||
| unit_net_weight | 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. |
body | number | false | ||
| unit_net_weight_and_serving_size_unit_type_id | 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. |
body | string | false | ||
| unit_price | Sale price of the product per unit, as a decimal. Required on create. Must be non-negative (>= 0). | body | number | false | ||
| unit_serving_size | 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. |
body | number | false | ||
| unit_type_id | ID of the product's unit type. Required on create. Cannot be changed once the product has any inventory. | body | string | false | ||
| units_per_case | Number of units in a case of the product. When provided must be greater than 0. | body | number | false | ||
| upc | Universal Product Code (UPC) for this product | body | string | false | 123456789012 | |
| vendor_id | 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. | body | string | false | ||
| wholesale_unit_price | The wholesale price of the product per unit. When provided must be non-negative (>= 0). | body | number | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single product | ProductResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
ProductCategory
Delete a product category
Success scenario
DELETE /public/v1/product-categories/00000000-0000-0000-0000-0000000000dd
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiMjFhMDQ1M2EtYzY5YS00OTY5LTg1NTItMGZjYmEzY2JmYzg0IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTM2OCIsInR5cCI6ImFjY2VzcyJ9.JJHhvbvfWrjVEedugm4BCSYlsJCHkj3Lzv-fAaqZv6g
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 576279a2e0c7c2fb3f0b2dc8c5c37241-60c4ec70b861a73b-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 19979417a13798b9882379874efdcbce-3736796efec2df5d-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Soft-delete the product category: it stops appearing in list and fetch responses, while the row is retained. Returns 404 if the category does not exist for your company or is already deleted.
Deleting also removes this category — and each of its subcategories — from any price tier filters that referenced them, so those price tiers stop scoping by the removed category. The deleted category's name stays reserved: it cannot be reused by a new category until the deleted one is renamed.
Required permission: settings_permissions_product_categories.
Request
DELETE /public/v1/product-categories/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the product category to delete, as returned in a category's id field. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a product category
Success scenario
GET /public/v1/product-categories/00000000-0000-0000-0000-000000000092
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTdlMzcxOGEtMjY4NC00NWI5LTlkMWMtZTZiYTQ2MTZlOTcyIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODk5IiwidHlwIjoiYWNjZXNzIn0.K5gj6dimLb2fedzeK5k1MlHy8A1pAm-6u1X7Bmf5SbQ
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 95c14cd6ecfee084d27471a35487d34a-30ce2558d695689a-0
{
"data": {
"id": "00000000-0000-0000-0000-000000000092",
"inserted_datetime": "2026-08-24T16:01:06.080313Z",
"name": "Edibles",
"official_product_category_id": "OPC_4",
"subcategories": [
{
"id": "00000000-0000-0000-0000-00000000008b",
"name": "Gummies"
}
],
"updated_datetime": "2026-08-24T16:01:06.080313Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4b096fbe1168541c97dbe3132a819529-f2a6d27ea3224094-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Fetch a single product category by ID, including its subcategories. Scoped to your company: returns 404 if no category with that ID exists for your company or if it has been soft-deleted.
Required permission: settings_permissions_product_categories.
Request
GET /public/v1/product-categories/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the product category, as returned in a category's id field. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single product category | ProductCategoryResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get product categories
Success scenario
GET /public/v1/product-categories
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZGFhOTQwOGEtMzIxOC00NWFmLTg2MjctMjk5NTJmOGNmNGRmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NjkwIiwidHlwIjoiYWNjZXNzIn0.EJ1Zq9dUiKZOViV1NN1KpIU9BuCRyrxUAQumoYS_E68
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b3e7795944757a914220cd0acd443515-a1a52d7c20c74f62-0
{
"data": [
{
"id": "00000000-0000-0000-0000-00000000006c",
"inserted_datetime": "2025-01-01T00:00:00.000000Z",
"name": "PC1",
"official_product_category_id": "OPC_3",
"subcategories": [
{
"id": "00000000-0000-0000-0000-00000000006f",
"name": "SC1"
}
],
"updated_datetime": "2026-08-24T16:01:05.289294Z"
},
{
"id": "00000000-0000-0000-0000-00000000006d",
"inserted_datetime": "2025-01-02T00:00:00.000000Z",
"name": "PC2",
"official_product_category_id": "OPC_3",
"subcategories": [],
"updated_datetime": "2026-08-24T16:01:05.291792Z"
},
{
"id": "00000000-0000-0000-0000-00000000006e",
"inserted_datetime": "2025-01-03T00:00:00.000000Z",
"name": "PC3",
"official_product_category_id": "OPC_3",
"subcategories": [],
"updated_datetime": "2026-08-24T16:01:05.293714Z"
}
],
"next_page": "https://www.example.com/public/v1/product-categories?page[number]=2"
}
Error scenario: invalid page parameter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4ea2ba01072110642461dd5bb1c7d873-009a511ca72f076b-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
List the product categories belonging to the authenticated company, oldest-first by creation time. Soft-deleted categories are excluded, and results are scoped to your company only. Each entry also carries its subcategories.
Results are paged: the response returns a fixed-size page plus a next_page URL, which is null
on the last page.
Required permission: settings_permissions_product_categories.
Request
GET /public/v1/product-categories
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of product categories | ProductCategories |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a product category
Success scenario
POST /public/v1/product-categories
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMjlmYThlZWEtMGNjNS00NzM0LWFjYjQtZmFiNTAzZTRjMmU0IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTA2NSIsInR5cCI6ImFjY2VzcyJ9.rayzUHo-zPxfjVA-BQMHpASmT6-6hW5xfMfTCrfxIrc
{
"id": "00000000-0000-0000-0000-0000000000a7",
"name": "New"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 1b3ab9fe129f91e31677c9a61fbed44d-8ea231e5957661c8-0
{
"data": {
"id": "00000000-0000-0000-0000-0000000000a7",
"inserted_datetime": "2026-08-24T16:01:06.658921Z",
"name": "New",
"official_product_category_id": "OTHER",
"subcategories": [
{
"id": "00000000-0000-0000-0000-00000000009e",
"name": "Gummies"
}
],
"updated_datetime": "2026-08-24T16:01:06.703383Z"
}
}
Error scenario: official product category not found
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 98347a5458ad78460a00104c70aa9794-d484dea22fc2756c-0
{
"errors": [
{
"context": {},
"message": "does not exist",
"pointer": [
"official_product_category_id"
],
"section": "body"
}
]
}
Create or update a single product category. Include the id of an existing category to update
it in place; omit id to create a new one. A create returns 201, an update returns 200. Updates
are sparse — only the fields you send change, and omitted fields keep their current value.
name must be unique within your company. A name currently held by a soft-deleted category is
also rejected, so restore or rename that deleted category first. Names are trimmed of surrounding
whitespace and may not contain special characters.
official_product_category_id maps the category to Distru's system-defined official category
list and is required when creating. Once a category maps to a non-null official category that
mapping is locked: on update, omit it or resend the same value — sending a different value is
rejected. The one exception is a category that is currently unmapped (null), which can happen
for categories created before this mapping existed: you may set its mapping on an update, but
not change it afterward. This mapping also drives how the category is synced to your connected
point-of-sale integrations (Blaze, Dutchie, Treez), so treat it as permanent once chosen.
Subcategories are not editable through this endpoint; manage them with the product subcategory endpoints. Existing products keep their category assignment.
Required permission: settings_permissions_product_categories.
Request
POST /public/v1/product-categories
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the category to update. When present, that category is updated in place; when absent, a new category is created. | body | string | false | ||
| name | 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. | body | string | true | ||
| official_product_category_id | 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. |
body | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated product category | ProductCategoryResponse |
| 201 | The created product category | ProductCategoryResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
ProductGroup
Delete a product group
Success scenario
DELETE /public/v1/product-groups/00000000-0000-0000-0000-00000000006c
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZDY5OTYwODctYjM4NS00MzJmLWJlNjEtZjJlYzBhOGU3NDdlIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NjgzIiwidHlwIjoiYWNjZXNzIn0.vMxwceIXreSjBeFjgyzO5_Ryfx7Cgl_NCdylcjY93XY
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 24bc4b5ebc4511c01b6e142a4ac3b30c-7a823fd5e4c12ac3-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 84ff4d39bced0ea1913bf892e97e0eb8-8e0988ccbda5b9b6-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Permanently deletes the product group. This is a hard delete: the record is removed and cannot be recovered. Returns 404 if no group with that ID belongs to the company.
Deleting a group ripples to everything that referenced it: • Products in this group are un-grouped — their group becomes empty. The products themselves are not deleted. • The group is dropped from any menus that featured it as a collection. • Any price tier that targeted or excluded this group has that condition removed, which changes which products those price tiers apply to.
Required permission: settings_permissions_product_groups.
Request
DELETE /public/v1/product-groups/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The product group's Distru ID, as returned in the id field of any product group response. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a product group
Success scenario
GET /public/v1/product-groups/00000000-0000-0000-0000-00000000002a
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiOWE0ZWRkMzYtZTE3Yy00NTZmLTk1MDYtZmE1Y2NmMGFkYzFmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjQ4IiwidHlwIjoiYWNjZXNzIn0.jy5AjEtOc84x1fotwzeOx4Yab46USRy3T5wO3i8j4PQ
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: bbe7e6481e43c78e66938b8bce5f9f85-5b6b98a1c04a46d0-0
{
"data": {
"id": "00000000-0000-0000-0000-00000000002a",
"inserted_datetime": "2026-08-24T16:01:03.643377Z",
"name": "Flower - Indoor",
"updated_datetime": "2026-08-24T16:01:03.643377Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: fb4e233b95f327130f6d1bd750993837-d616dd581cf00c9c-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Fetches a single product group by its ID, scoped to the authenticated company. Returns 404 if no group with that ID belongs to the company.
Required permission: settings_permissions_product_groups.
Request
GET /public/v1/product-groups/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The product group's Distru ID, as returned in the id field of any product group response. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single product group | ProductGroupResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get product groups
Success scenario
GET /public/v1/product-groups
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNDE2OTVkNTctZmE2OC00Yjc4LTk5NzctZmU1ZTU2OGMzODA5IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTA4IiwidHlwIjoiYWNjZXNzIn0.-DkGal8PUaOgcawyq9yDzI94_mFzMRFnwDvyU7I4r1Y
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 7c2554e2778b306106149c208d0e2037-bc372e55b7229f34-0
{
"data": [
{
"id": "00000000-0000-0000-0000-00000000000e",
"inserted_datetime": "2026-08-24T16:01:02.715537Z",
"name": "PG1",
"updated_datetime": "2026-08-24T16:01:02.715537Z"
},
{
"id": "00000000-0000-0000-0000-00000000000f",
"inserted_datetime": "2026-08-24T16:01:02.717557Z",
"name": "PG2",
"updated_datetime": "2026-08-24T16:01:02.717557Z"
},
{
"id": "00000000-0000-0000-0000-000000000010",
"inserted_datetime": "2026-08-24T16:01:02.718567Z",
"name": "PG3",
"updated_datetime": "2026-08-24T16:01:02.718567Z"
}
],
"next_page": "https://www.example.com/public/v1/product-groups?page[number]=2"
}
Error scenario: invalid page parameter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: df8a282dd2d8800e8696ef09878dfad3-326a544aaf422415-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
Lists the authenticated company's product groups, oldest first (ordered by creation time ascending). Product groups are the categories a product can belong to: each product references at most one group, and groups are also used as targeting or exclusion filters on price tiers.
Results are paginated, up to 500 groups per page. When more rows exist the response includes a
next_page URL to fetch the following page; next_page is null on the last page. Filter the list
by the inserted_datetime and updated_datetime ranges.
Required permission: settings_permissions_product_groups.
Request
GET /public/v1/product-groups
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of product groups | ProductGroups |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a product group
Success scenario
POST /public/v1/product-groups
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiNmMwMjRjNjctZmI3Ni00NzU2LWE0NDctMDBkYjU4NDk2ZTI0IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzQ5IiwidHlwIjoiYWNjZXNzIn0.3aOBCmINbKSzY_Simaz2VpRBer44-HHxzc_GxxKApZw
{
"name": "Flower - Indoor"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 081f93d463e4dd1ca5ce76ae151396ba-605e80cf0ab39254-0
{
"data": {
"id": "00000000-0000-0000-0000-000000000039",
"inserted_datetime": "2026-08-24T16:01:03.990306Z",
"name": "Flower - Indoor",
"updated_datetime": "2026-08-24T16:01:03.990306Z"
}
}
Error scenario: missing name
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a695126c0263bc1250921a6d01edff40-bf0996007bca4f13-0
{
"errors": [
{
"context": {},
"message": "Please enter a value",
"pointer": [
"name"
],
"section": "body"
}
]
}
Creates or updates a single product group. Pass id to update the matching group; omit id
to create a new one. A create returns 201, an update returns 200.
name is the only editable field and is required on both create and update; the update
replaces the group's name. A group's name must be unique within the company, compared
case-insensitively (Flower and flower collide) — a duplicate is rejected with a 400. The
name 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.
Assigning products to a group is not done here: a product's group is set on the product itself, not on this endpoint. This endpoint only defines the group's identity.
Required permission: settings_permissions_product_groups.
Request
POST /public/v1/product-groups
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | body | string | false | ||
| name | 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. |
body | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated product group | ProductGroupResponse |
| 201 | The created product group | ProductGroupResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
ProductPosMapping
Create or update a product POS mapping
Success scenario
POST /public/v1/product-pos-mappings
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNTc5NTJlNzQtODkwOS00MmQxLWFiNmYtMmRjNTUxYTcyZGRlIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NjI0IiwidHlwIjoiYWNjZXNzIn0.nZX8410nFHUUzhE_Ro0pSZW-Pnbwt24bKqeO3IkNRm4
{
"blaze_product_id": "blaze_123",
"blaze_retailer_id": "0b11283b-0076-40f9-91b2-f6dd9c132591",
"product_id": "8f146a4f-e72f-42c4-83bc-9615e2fd8ca1"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: bfcc75dbb89b751e91fc15daaabdd014-5cb128affe7543bf-0
{
"data": {
"blaze_asset_id": null,
"blaze_product_id": "blaze_123",
"blaze_retailer_id": "0b11283b-0076-40f9-91b2-f6dd9c132591",
"id": "00000000-0000-0000-0000-000000000006",
"inserted_datetime": "2026-08-24T16:01:05.100594Z",
"pos_type": "BLAZE",
"product_id": "8f146a4f-e72f-42c4-83bc-9615e2fd8ca1",
"updated_datetime": "2026-08-24T16:01:05.100594Z"
}
}
Error scenario: product not found
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a2a947d30d4316e9ecb91e2744c50f92-5875dc5c1164fb8f-0
{
"errors": [
{
"message": "Product not found",
"pointer": [
"product_id"
]
}
]
}
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.
You do not send a POS type. It is derived from which POS fields you supply, and you must supply exactly one complete pair:
• Blaze: blaze_product_id + blaze_retailer_id (optionally blaze_asset_id).
• Dutchie: dutchie_product_id + dutchie_retailer_id.
• Treez: treez_product_id + treez_retailer_id (optionally treez_photo_url).
Supplying no complete pair, or fields belonging to more than one POS, returns 400 — a mapping targets one POS only.
Create 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.
Before the mapping is written, several things are checked and any failure returns 400 with a human-readable message:
• The product_id must be a product in your company.
• The supplied retailer must be a POS retailer connected to your company.
• 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.
• For Blaze only, the product's category must already have a Blaze category mapping configured for that retailer.
Uniqueness 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.
On 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.
This 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.
Required permission: products_permissions_edit.
Request
POST /public/v1/product-pos-mappings
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| blaze_asset_id | 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. |
body | string | false | ||
| blaze_product_id | 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. |
body | string | false | ||
| blaze_retailer_id | Distru ID of the connected Blaze retailer to scope the mapping to. Provide together with blaze_product_id. |
body | string | false | ||
| dutchie_product_id | 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. |
body | integer | false | ||
| dutchie_retailer_id | Distru ID of the connected Dutchie retailer to scope the mapping to. Provide together with dutchie_product_id. |
body | string | false | ||
| product_id | ID of the Distru product to map. Must be a product in your company. Required. | body | string | true | ||
| treez_photo_url | 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. |
body | string | false | ||
| treez_product_id | 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. |
body | string | false | ||
| treez_retailer_id | Distru id of the connected Treez retailer to scope the mapping to, as an integer. Provide together with treez_product_id. |
body | integer | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Updated existing mapping | ProductPosMappingResponse |
| 201 | Created new mapping | ProductPosMappingResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Delete a product POS mapping
Success scenario
DELETE /public/v1/product-pos-mappings/00000000-0000-0000-0000-00000000000c
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiMjhmZjgwOTItNjUzZC00NzEzLWJlYzMtNjdmZTY3MGQxMTNkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTI2OCIsInR5cCI6ImFjY2VzcyJ9.RH7o0e3dLt3oUe7skqgBfCVfZe5sqEJUsiKwXSzk-sk
Response
204
cache-control: max-age=0, private, must-revalidate
b3: b1e9dc863247492b3ec7bd2b3b6aca81-518b5ce0ac04597b-0
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.
This 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).
Returns 204 on success, or 404 if no mapping with that id belongs to your company.
Required permission: products_permissions_edit.
Request
DELETE /public/v1/product-pos-mappings/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a product POS mapping
Success scenario
GET /public/v1/product-pos-mappings/00000000-0000-0000-0000-00000000000b
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMzU1Nzc2ZTYtZTY5Zi00NWY0LWE2MzItMzJlNTAwNGVjMjFkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTA3NyIsInR5cCI6ImFjY2VzcyJ9.2roXP6dK8nhICR880cp2ZMhzqU4uxzetIPBiBc5sE-g
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cfd0c1fea94df6770f18745f0c10b285-31c7af7412d6ba8d-0
{
"data": {
"blaze_asset_id": null,
"blaze_product_id": "blaze_123",
"blaze_retailer_id": "256fc0d3-f6f9-4cc1-aaca-4394e6c8e054",
"id": "00000000-0000-0000-0000-00000000000b",
"inserted_datetime": "2026-08-24T16:01:06.749746Z",
"pos_type": "BLAZE",
"product_id": "680359a5-a73f-4846-b76a-f79342224877",
"updated_datetime": "2026-08-24T16:01:06.749746Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 687a3df023e83cc1f11040309582cd29-bd1e086cc0f76462-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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).
Returns 404 if no mapping with that id belongs to a product in your company.
Required permission: products_permissions_view.
Request
GET /public/v1/product-pos-mappings/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single product POS mapping | ProductPosMappingResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
List product POS mappings
Success scenario
GET /public/v1/product-pos-mappings
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNTcxMDE2YzgtN2YyNC00MjBiLTg2MTctZjdlNDRiYTBjZDllIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzU3IiwidHlwIjoiYWNjZXNzIn0.VPdHiLmSG9YCQ6Tw0nthcBJyoLr9V-ZzTunPdGWNPx8
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: adade6ce1c05ed713580b7c6e04f691f-1b8a7ab357a3ca8a-0
{
"data": [
{
"blaze_asset_id": null,
"blaze_product_id": "blaze_123",
"blaze_retailer_id": "78d87cd8-a59f-4fdf-862a-c4b36398b521",
"id": "00000000-0000-0000-0000-000000000002",
"inserted_datetime": "2026-08-24T16:01:04.165243Z",
"pos_type": "BLAZE",
"product_id": "9350f8f4-716f-4945-8a15-e222c8bd287d",
"updated_datetime": "2026-08-24T16:01:04.165243Z"
},
{
"dutchie_product_id": 456,
"dutchie_retailer_id": "a55c158d-2257-4f07-9869-210465e22ec2",
"id": "00000000-0000-0000-0000-000000000003",
"inserted_datetime": "2026-08-24T16:01:04.185774Z",
"pos_type": "DUTCHIE",
"product_id": "1d47cfcb-aed6-49c4-838b-f2959379ae73",
"updated_datetime": "2026-08-24T16:01:04.185774Z"
}
],
"next_page": "https://www.example.com/public/v1/product-pos-mappings?page[number]=2"
}
Error scenario: invalid product_id filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 8f78964076c42667e01055d4379ef96c-1e642c040e1a726a-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"product_id"
],
"section": "query"
}
]
}
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.
Filtering:
• Pass no filter to return every POS mapping for your company, across all POS systems and retailers.
• product_id returns every mapping for one Distru product.
• blaze_retailer_id, dutchie_retailer_id, or treez_retailer_id returns every mapping for one connected retailer of that POS.
At 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.
Required permission: products_permissions_view.
Request
GET /public/v1/product-pos-mappings
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| blaze_retailer_id | 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. |
query | string | false | ?blaze_retailer_id=456e7890-e89b-12d3-a456-426614174000 | |
| dutchie_retailer_id | 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. |
query | string | false | ?dutchie_retailer_id=789e0123-e89b-12d3-a456-426614174000 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| product_id | Return only mappings for this Distru product, given as its ID. Exact match. Cannot be combined with any retailer filter. | query | string | false | ?product_id=123e4567-e89b-12d3-a456-426614174000 | |
| treez_retailer_id | 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. | query | integer | false | ?treez_retailer_id=1024 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Success | ProductPosMappingsResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
ProductSubcategory
Delete a product subcategory
Success scenario
DELETE /public/v1/product-subcategories/00000000-0000-0000-0000-00000000006a
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYWY4OWUwZTAtZjBjYi00Yjk4LWEwYzgtMGQyNzIxNTQ5MTI3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NjgxIiwidHlwIjoiYWNjZXNzIn0.YAq8UYpQcjlkGDmFqNMpCLfDnknvlWztnb-xAIyiYYU
Response
204
cache-control: max-age=0, private, must-revalidate
b3: c68c8166f07b934b832e490777f7d57a-411c104de4834a33-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 0d46c4fe085c228882b26f11c86addb8-b3de96167f821547-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Deleting 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.
Returns 204 on success and 404 if no subcategory with that id exists beneath a live product category owned by your company.
Required permission: settings_permissions_product_categories.
Request
DELETE /public/v1/product-subcategories/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The product subcategory id, as returned in the id field of any subcategory response. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a product subcategory
Success scenario
GET /public/v1/product-subcategories/00000000-0000-0000-0000-000000000035
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiMjU2OWY3ZmItYTNiZS00YTUwLTgxZGQtYzY5NjNiMGY4Zjk1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzE2IiwidHlwIjoiYWNjZXNzIn0.tSiV_5mACkeomLKLJy2MFDZFQcrrb8mdSAk58wbR8ZQ
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 98439e5cd2e2e64f9e9ad74ecd5b97af-c9df2d24942c3dfb-0
{
"data": {
"category": {
"id": "00000000-0000-0000-0000-000000000031",
"name": "Edibles",
"official_product_category_id": "OPC_1"
},
"id": "00000000-0000-0000-0000-000000000035",
"inserted_datetime": "2026-08-24T16:01:03.910971Z",
"name": "Gummies",
"updated_datetime": "2026-08-24T16:01:03.910971Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 6b91cb357113a5659d7d4b68339b9bfa-2121b6def4bfde7f-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Required permission: settings_permissions_product_categories.
Request
GET /public/v1/product-subcategories/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The product subcategory id, as returned in the id field of any subcategory response. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single product subcategory | ProductSubcategoryResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get product subcategories
Success scenario
GET /public/v1/product-subcategories
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMWQ2MDZiY2QtZTI1Yy00ODFiLWFmNjYtYmExMzQwZTRmYmY4IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODMiLCJ0eXAiOiJhY2Nlc3MifQ.-XJ4Qqd0f8uYQoh2LEHUSbbSdrm0ZI-euWmGpD2yYu8
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 9b8d56abdc34c074d9b45ab45dd26c67-730a0cb9fa4863ab-0
{
"data": [
{
"category": {
"id": "00000000-0000-0000-0000-00000000000d",
"name": "C1",
"official_product_category_id": "OPC_0"
},
"id": "00000000-0000-0000-0000-00000000000d",
"inserted_datetime": "2025-01-01T00:00:00.000000Z",
"name": "SC1",
"updated_datetime": "2026-08-24T16:01:02.548628Z"
},
{
"category": {
"id": "00000000-0000-0000-0000-00000000000d",
"name": "C1",
"official_product_category_id": "OPC_0"
},
"id": "00000000-0000-0000-0000-00000000000e",
"inserted_datetime": "2025-01-02T00:00:00.000000Z",
"name": "SC2",
"updated_datetime": "2026-08-24T16:01:02.572926Z"
},
{
"category": {
"id": "00000000-0000-0000-0000-00000000000d",
"name": "C1",
"official_product_category_id": "OPC_0"
},
"id": "00000000-0000-0000-0000-00000000000f",
"inserted_datetime": "2025-01-03T00:00:00.000000Z",
"name": "SC3",
"updated_datetime": "2026-08-24T16:01:02.607021Z"
}
],
"next_page": "https://www.example.com/public/v1/product-subcategories?page[number]=2"
}
Error scenario: invalid page parameter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b950433f33c0977dd9b0d9ba8b3481cd-6cd10cf3ec6adeff-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
List product subcategories for the authenticated company, oldest first (ascending creation order).
A 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.
Optionally narrow the list to one parent category with ?category_id= (exact match on the parent category id).
Results 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.
Required permission: settings_permissions_product_categories.
Request
GET /public/v1/product-subcategories
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| category_id | Return only subcategories whose parent product category id matches this value exactly. Omit to return every subcategory across all of your categories. | query | string | false | ?category_id=b1c2d3e4-5f60-4a7b-8c9d-0e1f2a3b4c5d | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of product subcategories | ProductSubcategories |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a product subcategory
Success scenario
POST /public/v1/product-subcategories
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMGU0NDZhOGQtMmJlYS00M2U3LTk2NzktYTI1M2QwM2YxNDZkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDIxIiwidHlwIjoiYWNjZXNzIn0.0913iXXUeuaMtRcExB-_JcX8GNpnPaIvknRV--koYC8
{
"name": "Gummies",
"product_category_id": "00000000-0000-0000-0000-000000000041"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 642bbd994768cf0d1feaeeb66eee2f45-97b138baad5f1235-0
{
"data": {
"category": {
"id": "00000000-0000-0000-0000-000000000041",
"name": "Edibles",
"official_product_category_id": "OPC_2"
},
"id": "00000000-0000-0000-0000-000000000044",
"inserted_datetime": "2026-08-24T16:01:04.297451Z",
"name": "Gummies",
"updated_datetime": "2026-08-24T16:01:04.297451Z"
}
}
Error scenario: missing name
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 0b0e0bdd3b5e20111628f716cbff379e-0e71561c644cc9b2-0
{
"errors": [
{
"context": {},
"message": "Please enter a name",
"pointer": [
"name"
],
"section": "body"
}
]
}
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.
On 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.
The 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.
Subcategories 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.
Required permission: settings_permissions_product_categories.
Request
POST /public/v1/product-subcategories
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | body | string | false | ||
| name | 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. | body | string | true | |
| product_category_id | 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. | body | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated product subcategory | ProductSubcategoryResponse |
| 201 | The created product subcategory | ProductSubcategoryResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Purchase
Delete a purchase
Success scenario
DELETE /public/v1/purchases/00000000-0000-0000-0000-000000000084
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzMsImlhdCI6MTc4NzU4NzI3MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmE0NTNlYjgtNjRkZS00NTNlLWI0NGEtOGE3ZWYxYTBjYzk1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzE0MiIsInR5cCI6ImFjY2VzcyJ9.prQ9oJP7X1X_OLC9F0AlSUp3TwgJHRCOSQn0mm-JP5o
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 8c2f5a1d94e3ba702811d6c7d10286db-4b7edcd33594b7aa-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cf914631d8868f824545ba38e52cf1ab-aa618aa77fb9cfbe-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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).
A 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.
Deleting a received purchase (PARTIALLY_RECEIVED or COMPLETED) pulls its received quantities back out of 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.
Other 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.
Required permission: purchases_permissions_delete (plus access to the purchase under team restrictions).
Request
DELETE /public/v1/purchases/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a purchase
Success scenario
GET /public/v1/purchases/00000000-0000-0000-0000-000000000084
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4ODMsImlhdCI6MTc4NzU4NzI4MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiY2M3YmJiMjctNDk4NS00OGQxLWE1MTItYWNlY2Y5Y2FhMjRiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjgyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDcxOCIsInR5cCI6ImFjY2VzcyJ9.YSNaXoUMZnRmHYPsXONy_P7C2tE17VpeTlvcRS5yNqQ
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: baf4f4e7615c9264d54243750d3fac07-40559f65c1cc4c09-0
{
"data": {
"billing_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000d55",
"id": "00000000-0000-0000-0000-000000000463",
"license_id": null,
"license_number": null,
"name": "Place 1121"
},
"biotrack_id": null,
"charges": [
{
"id": "f983d9c4-2da4-4659-830d-db24af43175f",
"inserted_datetime": "2026-08-24T16:01:23.887311Z",
"name": "C1",
"percent": "10.0000",
"price": "1.00",
"tax": {
"id": "00000000-0000-0000-0000-000000000016",
"name": "T1"
},
"type": "CHARGE",
"unit_type": "PERCENT"
}
],
"company": {
"id": "00000000-0000-0000-0000-000000000800",
"name": "Company 3407",
"updated_datetime": "2026-08-24T16:01:23.811777Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-4691@example.com",
"full_name": "FirstName9492 LastName9493",
"id": "00000000-0000-0000-0000-000000001275",
"inserted_datetime": "2026-08-24T16:01:23.835889Z",
"role": {
"id": "00000000-0000-0000-0000-000000001285",
"name": "Admin 4740"
}
},
"custom_data": [
{
"id": 118,
"name": "Custom Field 92",
"value": "Custom Field Value 1"
}
],
"description": null,
"due_datetime": "2026-08-24T16:01:23.837828Z",
"id": "00000000-0000-0000-0000-000000000084",
"inserted_datetime": "2026-08-24T16:01:23.838208Z",
"items": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-0000000004f2",
"name": "B3947"
},
"compliance_quantity": null,
"id": "29bbc2f0-5c66-4997-b4c2-3aafb90ab165",
"is_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000d55",
"id": "00000000-0000-0000-0000-000000000462",
"license_id": null,
"name": "Place 1120"
},
"package": null,
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "bc04c868-9f20-47cd-b4ed-762e95d5a1b6",
"name": "Product 3939",
"sku": "sku 3940",
"updated_datetime": "2026-08-24T16:01:23.844916Z"
},
"quantity": "15.000000000",
"received_quantity": "0.000000000"
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-0000000004f3",
"name": "B3948"
},
"compliance_quantity": null,
"id": "f85a30a9-5c18-4e99-a4ae-a826aa30978a",
"is_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000d55",
"id": "00000000-0000-0000-0000-000000000462",
"license_id": null,
"name": "Place 1120"
},
"package": null,
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "e41d82c7-c064-4dda-b29a-8363535e1328",
"name": "Product 3941",
"sku": "sku 3942",
"updated_datetime": "2026-08-24T16:01:23.851625Z"
},
"quantity": "10.000000000",
"received_quantity": "0.000000000"
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-0000000004f4",
"name": "B3949"
},
"compliance_quantity": null,
"id": "99cd84a2-07bb-4dfe-ac61-4455ed42afe1",
"is_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000d55",
"id": "00000000-0000-0000-0000-000000000462",
"license_id": null,
"name": "Place 1120"
},
"package": null,
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "e4eea335-de1c-4205-8d44-11be5d4692c7",
"name": "Product 3943",
"sku": "sku 3944",
"updated_datetime": "2026-08-24T16:01:23.860199Z"
},
"quantity": "5.000000000",
"received_quantity": "0.000000000"
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-0000000004f5",
"name": "B3950"
},
"compliance_quantity": null,
"id": "de046e57-3d27-4fae-bf73-fdd091360729",
"is_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000d55",
"id": "00000000-0000-0000-0000-000000000462",
"license_id": null,
"name": "Place 1120"
},
"package": null,
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "b11a8fc5-a3af-4e2f-9b68-0496d6db9c47",
"name": "Product 3945",
"sku": "sku 3946",
"updated_datetime": "2026-08-24T16:01:23.867064Z"
},
"quantity": "2.000000000",
"received_quantity": "0.000000000"
}
],
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000d55",
"id": "00000000-0000-0000-0000-000000000462",
"license_id": null,
"license_number": null,
"name": "Place 1120"
},
"metrc_transfer_id": null,
"order_datetime": "2026-08-24T16:01:23.837827Z",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-4691@example.com",
"full_name": "FirstName9492 LastName9493",
"id": "00000000-0000-0000-0000-000000001275",
"inserted_datetime": "2026-08-24T16:01:23.835889Z",
"role": {
"id": "00000000-0000-0000-0000-000000001285",
"name": "Admin 4740"
}
},
"paid": "100.01",
"payment_status": "NOT_PAID",
"payments": [
{
"amount": "100.01",
"company": {
"id": "00000000-0000-0000-0000-000000000800",
"name": "Company 3407",
"updated_datetime": "2026-08-24T16:01:23.811777Z"
},
"credit_uses": null,
"description": "Payment for purchase",
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-000000000034",
"inserted_datetime": "2026-08-24T16:01:23.895570Z",
"invoice": null,
"overpayment_credits": null,
"payment_date": "2020-01-01T00:00:00.000000Z",
"payment_datetime": "2020-01-01T00:00:00.000000Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000045",
"inserted_datetime": "2026-08-24T16:01:23.894371Z",
"name": "Payment Method 68",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:23.894371Z"
},
"payment_number": "PYT-1",
"payment_type": "PURCHASE",
"purchase": {
"id": "00000000-0000-0000-0000-000000000084",
"purchase_number": "Purchase #115",
"status": "PENDING",
"total": "32.00"
},
"quickbooks_deposit_account_id": null,
"status": "POSTED",
"updated_datetime": "2026-08-24T16:01:23.895570Z"
}
],
"purchase_number": "Purchase #115",
"qb_bill_id": null,
"status": "PENDING",
"supplier_location": null,
"total": "32.00",
"updated_datetime": "2026-08-24T16:01:23.838208Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: e17f46bef59615706bd1b9c4b4635f1b-62708fc370a9fa33-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get a single purchase by its ID, returned as the full purchase shape (line items, charges, active payments, and custom data). Draft purchases and purchases outside your company are not found (404).
Required permission: purchases_permissions_view, plus access to the purchase under the authenticated user's team restrictions.
Request
GET /public/v1/purchases/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | Purchase ID | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single purchase | PurchaseResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get purchases
Success scenario
GET /public/v1/purchases
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjksImlhdCI6MTc4NzU4NzI2OSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMmZhYzE4MGEtMWExZC00OGJmLTk2ZjQtODg0NjFkN2NjYjE4IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY4LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTg5NyIsInR5cCI6ImFjY2VzcyJ9.9rsBFm4oYtoraao_3C-8fjsBM4EWgAfS2krq31kYL9g
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4327d5481f8d5d8f42fc47cc34bf1433-c5ace83e60446a54-0
{
"data": [
{
"billing_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000562",
"id": "00000000-0000-0000-0000-00000000018a",
"license_id": null,
"license_number": null,
"name": "Place 393"
},
"biotrack_id": null,
"charges": [],
"company": {
"id": "00000000-0000-0000-0000-00000000025d",
"name": "Company 1421",
"updated_datetime": "2026-08-24T16:01:09.417661Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1983@example.com",
"full_name": "FirstName4040 LastName4041",
"id": "00000000-0000-0000-0000-0000000007c9",
"inserted_datetime": "2026-08-24T16:01:09.464936Z",
"role": {
"id": "00000000-0000-0000-0000-00000000078d",
"name": "Admin 1932"
}
},
"custom_data": [
{
"id": 70,
"name": "Custom Field 47",
"value": null
}
],
"description": null,
"due_datetime": "2026-08-24T16:01:09.469264Z",
"id": "00000000-0000-0000-0000-000000000018",
"inserted_datetime": "2026-08-24T16:01:09.469721Z",
"items": [
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000012b",
"name": "B897"
},
"compliance_quantity": null,
"id": "c67631b5-5e3c-4e4b-bc88-c2ff16f740e0",
"is_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000562",
"id": "00000000-0000-0000-0000-000000000187",
"license_id": null,
"name": "Place 390"
},
"package": null,
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "fef6457f-b249-4fae-a6f0-c57c842d76a6",
"name": "Product 880",
"sku": "sku 881",
"updated_datetime": "2026-08-24T16:01:09.486268Z"
},
"quantity": "15.000000000",
"received_quantity": "0.000000000"
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000012c",
"name": "B898"
},
"compliance_quantity": null,
"id": "f34b1260-7480-40e0-97fe-2c3888cbe52e",
"is_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000562",
"id": "00000000-0000-0000-0000-000000000187",
"license_id": null,
"name": "Place 390"
},
"package": null,
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "ab9e46ce-1974-457d-a730-e9d0a8976290",
"name": "Product 887",
"sku": "sku 888",
"updated_datetime": "2026-08-24T16:01:09.503776Z"
},
"quantity": "10.000000000",
"received_quantity": "0.000000000"
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000012d",
"name": "B899"
},
"compliance_quantity": null,
"id": "6aa5ed90-5097-4312-94dd-ec7691b60bd0",
"is_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000562",
"id": "00000000-0000-0000-0000-000000000187",
"license_id": null,
"name": "Place 390"
},
"package": null,
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "2ee124c9-5577-42eb-b7d2-7a5723a89733",
"name": "Product 891",
"sku": "sku 892",
"updated_datetime": "2026-08-24T16:01:09.521149Z"
},
"quantity": "5.000000000",
"received_quantity": "0.000000000"
},
{
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000012e",
"name": "B900"
},
"compliance_quantity": null,
"id": "48fe58e8-321f-41d0-b3e6-6d89a0f0883a",
"is_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000562",
"id": "00000000-0000-0000-0000-000000000187",
"license_id": null,
"name": "Place 390"
},
"package": null,
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "bdc57a8f-5550-4235-8cdf-189a7915edf8",
"name": "Product 895",
"sku": "sku 896",
"updated_datetime": "2026-08-24T16:01:09.539464Z"
},
"quantity": "2.000000000",
"received_quantity": "0.000000000"
}
],
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000562",
"id": "00000000-0000-0000-0000-000000000187",
"license_id": null,
"license_number": null,
"name": "Place 390"
},
"metrc_transfer_id": null,
"order_datetime": "2026-08-24T16:01:09.469263Z",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-1983@example.com",
"full_name": "FirstName4040 LastName4041",
"id": "00000000-0000-0000-0000-0000000007c9",
"inserted_datetime": "2026-08-24T16:01:09.464936Z",
"role": {
"id": "00000000-0000-0000-0000-00000000078d",
"name": "Admin 1932"
}
},
"paid": "0",
"payment_status": "NOT_PAID",
"payments": [],
"purchase_number": "Purchase #23",
"qb_bill_id": null,
"status": "PENDING",
"supplier_location": null,
"total": "32.00",
"updated_datetime": "2026-08-24T16:01:09.469721Z"
},
{
"billing_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000562",
"id": "00000000-0000-0000-0000-000000000182",
"license_id": null,
"license_number": null,
"name": "Place 385"
},
"biotrack_id": null,
"charges": [
{
"id": "d3a03332-dc82-4045-824d-dbe9c02316b3",
"inserted_datetime": "2026-08-24T16:01:09.398583Z",
"name": "C1",
"percent": "10.0000",
"price": "1.00",
"tax": {
"id": "00000000-0000-0000-0000-000000000011",
"name": "T1"
},
"type": "CHARGE",
"unit_type": "PERCENT"
}
],
"company": {
"id": "00000000-0000-0000-0000-000000000258",
"name": "Company 1409",
"updated_datetime": "2030-11-01T00:00:00.000000Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "purchase-owner@example.com",
"full_name": "FirstName3862 LastName3863",
"id": "00000000-0000-0000-0000-000000000771",
"inserted_datetime": "2026-08-24T16:01:09.250825Z",
"role": {
"id": "00000000-0000-0000-0000-000000000794",
"name": "Admin 1939"
}
},
"custom_data": [
{
"id": 70,
"name": "Custom Field 47",
"value": "Custom Field Value 1"
}
],
"description": "A description of this purchase",
"due_datetime": "2020-01-01T00:00:01.000000Z",
"id": "00000000-0000-0000-0000-000000000017",
"inserted_datetime": "2020-01-01T00:00:03.000000Z",
"items": [
{
"batch": null,
"compliance_quantity": "1.0000",
"id": "3aab349b-f1c9-4e22-b18e-354c776e251c",
"is_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000562",
"id": "00000000-0000-0000-0000-000000000175",
"license_id": "00000000-0000-0000-0000-000000000049",
"name": "Place 372"
},
"package": {
"batch_number": "B1",
"compliance_label": "ABCDEF012345670000000092",
"distru_status": "ACTIVE",
"id": "00000000-0000-0000-0000-000000000032",
"license_id": "00000000-0000-0000-0000-00000000004c",
"location_id": "00000000-0000-0000-0000-00000000017b",
"metrc_id": 91,
"metrc_label": "ABCDEF012345670000000092",
"quantity": "10.000000000",
"quantity_active": "10.000000000",
"status": "active"
},
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "44381c30-e358-466a-83a8-8dffb6832f4a",
"name": "P1",
"sku": "SKU1",
"updated_datetime": "2023-11-02T00:00:00.000000Z"
},
"quantity": "1.000000000",
"received_quantity": "1.000000000"
}
],
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000562",
"id": "00000000-0000-0000-0000-000000000181",
"license_id": null,
"license_number": null,
"name": "Place 384"
},
"metrc_transfer_id": null,
"order_datetime": "2020-01-01T00:00:02.000000Z",
"owner": {
"banned": false,
"deleted_at": null,
"email": "purchase-owner@example.com",
"full_name": "FirstName3862 LastName3863",
"id": "00000000-0000-0000-0000-000000000771",
"inserted_datetime": "2026-08-24T16:01:09.250825Z",
"role": {
"id": "00000000-0000-0000-0000-000000000794",
"name": "Admin 1939"
}
},
"paid": "0",
"payment_status": "NOT_PAID",
"payments": [],
"purchase_number": "SO-123",
"qb_bill_id": null,
"status": "COMPLETED",
"supplier_location": null,
"total": "10.00",
"updated_datetime": "2020-01-01T00:00:04.000000Z"
}
],
"next_page": null
}
Error scenario: invalid status filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 29bd97d64acf489fcfca9ee78cc109ca-2b94056cf09833fe-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"statuses"
],
"section": "query"
}
]
}
List purchase orders, most recent Order Date first, filtered by the query parameters below. Each entry is the full purchase shape (line items, charges, active payments, custom data). Draft purchases are never returned.
Results are paginated: the response data array holds one page, and next_page is the URL for the following page (null on the last page).
Required 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.
Request
GET /public/v1/purchases
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_batch_numbers | 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). | query | array | false | ?batch_batch_numbers[]=BN-2001 | |
| batch_ids | 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). | query | array | false | ?batch_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| biotrack_ids | 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. |
query | array | false | ?biotrack_ids[]=0000000123 | |
| company_group_ids | 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. | query | array | false | ?company_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| company_ids | 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. |
query | array | false | ?company_ids[]=550e8400-e29b-41d4-a716-446655440000&company_ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| custom_data | 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. |
query | object | false | ?custom_data[101]=Blue&custom_data[102]=Wholesale | |
| due_datetime | 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. |
query | string | false | ,2022-07-10T00:00:00Z | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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). |
query | string | false | 2022-07-10T00:00:00Z, | |
| license_number | 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. |
query | string | false | ?license_number=C11-0000123-LIC | |
| location_ids | 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. |
query | array | false | ?location_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| metrc_transfer_ids | 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. |
query | array | false | ?metrc_transfer_ids[]=987654 | |
| order_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z,2022-07-11T00:00:00Z | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| package_batch_numbers | 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). | query | array | false | ?package_batch_numbers[]=BN-1001 | |
| package_compliance_labels | 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). | query | array | false | ?package_compliance_labels[]=1A4000000000000000000123 | |
| package_ids | 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). | query | array | false | ?package_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| payment_statuses | 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:
NOT_PAID PARTIALLY_PAID FULLY_PAID OVER_PAID |
query | array | false | ?payment_statuses[]=NOT_PAID&payment_statuses[]=PARTIALLY_PAID | |
| product_brand_ids | 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). | query | array | false | ?product_brand_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_category_ids | 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). | query | array | false | ?product_category_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_group_ids | 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). | query | array | false | ?product_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_ids | 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). | query | array | false | ?product_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_skus | 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). | query | array | false | ?product_skus[]=SKU-1001&product_skus[]=SKU-1002 | |
| product_strain_ids | 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). | query | array | false | ?product_strain_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_subcategory_ids | 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). | query | array | false | ?product_subcategory_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_tag_ids | 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). | query | array | false | ?product_tag_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_vendor_ids | 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). | query | array | false | ?product_vendor_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| purchase_number | 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. |
query | string | false | ?purchase_number=PO-10 | |
| purchase_numbers | 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. |
query | array | false | ?purchase_numbers[]=PO-1001&purchase_numbers[]=PO-1002 | |
| statuses | 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 for what each value means.COMPLETED DELIVERING PENDING PARTIALLY_RECEIVED PROCESSING |
query | array | false | ?statuses[]=PENDING&statuses[]=PROCESSING | |
| total | 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. |
query | string | false | 100,500 | |
| updated_datetime | Filter purchases by when they were last modified in Distru. ISO8601 range lower,upper (both UTC); omit either side for an open-ended bound. |
query | string | false | ,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of purchases | Purchases |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Insert a payment for a purchase
Success scenario
POST /public/v1/purchases/00000000-0000-0000-0000-00000000006a/payments
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4ODEsImlhdCI6MTc4NzU4NzI4MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMWUwN2UwZjktYjJmOC00OGI3LTlhMDMtNGM5ZjYwNmY4NDM0IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjgwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDQ1MSIsInR5cCI6ImFjY2VzcyJ9.5GU4xAEuz4X9o3fVYwTl2xZyrgDQrTjMc_1EhziQUjw
{
"amount": 100.01,
"description": "Payment for purchase",
"payment_datetime": "2020-01-01T00:00:00.000000Z",
"payment_method_id": "00000000-0000-0000-0000-000000000042",
"quickbooks_deposit_account_id": "QBD-123"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f39bad725f7487fde4fc89615bd8741b-d49bc43186592968-0
{
"data": {
"amount": "100.01",
"company": {
"id": "00000000-0000-0000-0000-00000000072c",
"name": "Company 3154",
"updated_datetime": "2026-08-24T16:01:21.339478Z"
},
"credit_uses": null,
"description": "Payment for purchase",
"fully_paid_with_credits": false,
"id": "00000000-0000-0000-0000-000000000032",
"inserted_datetime": "2026-08-24T16:01:21.364921Z",
"invoice": null,
"overpayment_credits": null,
"payment_date": "2020-01-01T00:00:00.000000Z",
"payment_datetime": "2020-01-01T00:00:00.000000Z",
"payment_method": {
"active": true,
"deleted_at": null,
"id": "00000000-0000-0000-0000-000000000042",
"inserted_datetime": "2026-08-24T16:01:21.348589Z",
"name": "Payment Method 0",
"qb_payment_method_id": null,
"type": "CREDIT_CARD",
"updated_datetime": "2026-08-24T16:01:21.348589Z"
},
"payment_number": "PYT-0000001",
"payment_type": "PURCHASE",
"purchase": {
"id": "00000000-0000-0000-0000-00000000006a",
"purchase_number": "Purchase #93",
"status": "PENDING",
"total": "32.00"
},
"quickbooks_deposit_account_id": "QBD-123",
"quickbooks_deposit_account_name": "QBD-NAME",
"status": "POSTED",
"updated_datetime": "2026-08-24T16:01:21.364921Z"
}
}
Error scenario: payment on draft purchase
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: bdfe9e1163973af545262c9b279d6e78-0a2a05636d61940f-0
{
"errors": [
{
"context": {},
"message": "Cannot create payment for a draft purchase",
"pointer": [
"id"
]
}
]
}
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.
The 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.
A 404 is returned if the purchase does not exist or is not in your company.
Required permission: purchases_permissions_make_payments. The authenticated user must also be allowed to view purchases under their team restrictions.
Request
POST /public/v1/purchases/{id}/payments
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| amount | 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. |
body | decimal | true | ||
| description | A free-text note describing the payment. | body | string | true | ||
| id | The purchase's ID — the id returned by the list and show purchase endpoints. |
path | string | true | ||
| payment_datetime | The datetime the payment was made, as a full ISO8601 datetime (e.g. 2026-08-18T00:00:00Z). |
body | string | true | ||
| payment_method_id | ID of the payment method this payment was made with (e.g. cash, check, ACH). Must reference an existing payment method in your company. | body | string | true | ||
| quickbooks_deposit_account_id | 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" | body | string | false | ||
| quickbooks_deposit_account_name | 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" | body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single payment | PaymentResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Upsert a purchase order
Success scenario
POST /public/v1/purchases
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzgsImlhdCI6MTc4NzU4NzI3OCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYzlkYTVjMDQtN2NhNC00OGMxLWJkNzgtY2YyNmJmMmQ1Nzc3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3Mjc3LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6Mzg4NCIsInR5cCI6ImFjY2VzcyJ9.7CtHqltrepbKbCSE8a-N5v_7181d1IzNsKJDkdqPK_k
{
"billing_location_id": "00000000-0000-0000-0000-000000000348",
"charges": [
{
"name": "C1",
"percent": "10.0000",
"type": "CHARGE",
"unit_type": "PERCENT"
},
{
"name": "C2",
"price": "-5.0000",
"type": "DISCOUNT",
"unit_type": "PRICE"
}
],
"company_id": "00000000-0000-0000-0000-0000000005ea",
"custom_data": {
"113": [
"A",
"B"
]
},
"description": "A description of this purchase",
"due_datetime": "2020-01-30T00:00:00.000000Z",
"items": [
{
"location_id": "00000000-0000-0000-0000-000000000345",
"price": "10.000000000",
"product_id": "88ab9070-3099-4bab-bc95-6ce04a53557b",
"quantity": "1.000000000"
}
],
"location_id": "00000000-0000-0000-0000-000000000345",
"order_datetime": "2020-01-01T00:00:00.000000Z",
"owner_id": "00000000-0000-0000-0000-000000000f2c"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cf3343f39d7879c229b42006e8d4913f-6dd39a9c69e1f6ea-0
{
"data": {
"billing_location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000ac3",
"id": "00000000-0000-0000-0000-000000000348",
"license_id": null,
"license_number": null,
"name": "Place 838"
},
"biotrack_id": null,
"charges": [
{
"id": "5920c781-ff09-4df4-bd15-17a015fc27d6",
"inserted_datetime": "2026-08-24T16:01:18.101621Z",
"name": "C1",
"percent": "10.0000",
"price": "1.00",
"type": "CHARGE",
"unit_type": "PERCENT"
},
{
"id": "00fb6a97-056a-4f00-b957-120c0c94bb0c",
"inserted_datetime": "2026-08-24T16:01:18.103333Z",
"name": "C2",
"percent": null,
"price": "-5.00",
"type": "DISCOUNT",
"unit_type": "PRICE"
}
],
"company": {
"id": "00000000-0000-0000-0000-0000000005ea",
"name": "Company 2754",
"updated_datetime": "2026-08-24T16:01:17.788154Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "user1@a.com",
"full_name": "John Foo",
"id": "00000000-0000-0000-0000-000000000f2c",
"inserted_datetime": "2026-08-24T16:01:17.827992Z",
"role": {
"id": "00000000-0000-0000-0000-000000000f64",
"name": "Admin 3939"
}
},
"custom_data": [
{
"id": 113,
"name": "Custom Field 87",
"value": "A,B"
}
],
"description": "A description of this purchase",
"due_datetime": "2020-01-30T00:00:00.000000Z",
"id": "00000000-0000-0000-0000-000000000049",
"inserted_datetime": "2026-08-24T16:01:18.099787Z",
"items": [
{
"batch": null,
"compliance_quantity": null,
"id": "77e69e7f-d18a-4e2e-a010-c393f89863b9",
"is_sample": false,
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000ac3",
"id": "00000000-0000-0000-0000-000000000345",
"license_id": "00000000-0000-0000-0000-0000000000cf",
"name": "Place 835"
},
"package": null,
"price": "10.000000000",
"price_base": "10.000000000",
"product": {
"id": "88ab9070-3099-4bab-bc95-6ce04a53557b",
"name": "P1",
"sku": "SKU1",
"updated_datetime": "2026-08-24T16:01:18.070719Z"
},
"quantity": "1.000000000",
"received_quantity": "0.000000000"
}
],
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-000000000ac3",
"id": "00000000-0000-0000-0000-000000000345",
"license_id": "00000000-0000-0000-0000-0000000000cf",
"license_number": "CDPH-00000210",
"name": "Place 835"
},
"metrc_transfer_id": null,
"order_datetime": "2020-01-01T00:00:00.000000Z",
"owner": {
"banned": false,
"deleted_at": null,
"email": "user1@a.com",
"full_name": "John Foo",
"id": "00000000-0000-0000-0000-000000000f2c",
"inserted_datetime": "2026-08-24T16:01:17.827992Z",
"role": {
"id": "00000000-0000-0000-0000-000000000f64",
"name": "Admin 3939"
}
},
"paid": "0",
"payment_status": "NOT_PAID",
"payments": [],
"purchase_number": "PO-0000001",
"qb_bill_id": null,
"status": "PENDING",
"supplier_location": null,
"total": "6.00",
"updated_datetime": "2026-08-24T16:01:18.108702Z"
}
}
Error scenario: owner not found
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ffb756294b60e1ce3c8087ab0078e9f1-37a888ee30a8ed50-0
{
"errors": [
{
"context": {},
"message": "Owner does not exist",
"pointer": [
"owner_id"
],
"section": "body"
}
]
}
Error scenario: received_quantity on non-received status
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 351204955dc4431c8619e8ce98709f4d-543008cdcb3b2ad2-0
{
"errors": [
{
"context": {},
"message": "can only be set when the purchase status is PARTIALLY_RECEIVED",
"pointer": [
"items",
0,
"received_quantity"
],
"section": "body"
}
]
}
Error scenario: charge missing name
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 972deb4b6cba0964f2f705bfa3483f45-0f585739b27e5150-0
{
"errors": [
{
"context": {
"id": "432f6f59-466b-4215-8795-8a25acaeb53d"
},
"message": "What is this charge for?",
"pointer": [
"charges",
0,
"name"
],
"section": "body"
}
]
}
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.
See the status field on the purchase response for what each value means. Allowed transitions: PENDING, PROCESSING, and DELIVERING may move freely between one another and forward to PARTIALLY_RECEIVED or COMPLETED. Once a purchase reaches PARTIALLY_RECEIVED or COMPLETED it has received inventory and can no longer move back to PENDING, PROCESSING, or DELIVERING (it may still move between PARTIALLY_RECEIVED and COMPLETED). PARTIALLY_RECEIVED is not allowed for purchases that contain package-tracked items.
For a PARTIALLY_RECEIVED purchase, set each line's received_quantity to the amount received so far. In a subsequent call you may decrease a line's received_quantity, or delete a line that has a positive received_quantity, as long as the previously-received quantity has not yet been consumed elsewhere in Distru (e.g. sold, transferred, or adjusted); otherwise the change is rejected.
To match the purchase with an incoming compliance transfer, pass a top-level metrc_transfer_id or biotrack_id. This is only valid with status = COMPLETED, and requires the purchase's location_id to be on the license that received the transfer; the referenced incoming transfer must exist or the request is rejected. On each line item, identify the package it maps to with metrc_package_id (Metrc) or biotrack_id (BioTrack) and give its compliance_quantity. Once matched, a purchase is locked at COMPLETED and its transfer association cannot be changed.
Required permission: purchases_permissions_create to create a new purchase order, purchases_permissions_edit (and access to the purchase under team restrictions) to update an existing purchase order.
Request
POST /public/v1/purchases
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| billing_location_id | The ID of the location used as the billing address for this purchase order. Required on create; on update, omit to leave it unchanged. | body | string | false | ||
| biotrack_id | 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. |
body | string | false | ||
| charges | The extra lines added on top of the purchase order's items — fees, discounts, or taxes. Each entry follows the 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. |
body | array | false | ||
| company_id | 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. | body | string | false | ||
| custom_data | 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. | body | object | false | {"101":"Some text value","102":"2026-08-18T00:00:00.000-07:00","103":["Option A","Option B"]} | |
| description | A free-text description of the purchase order. Optional; send null to clear it. | body | string | false | ||
| due_datetime | 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. |
body | string | false | ||
| id | 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. | body | string | false | ||
| items | The products being purchased, one entry per line. Each entry follows the 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. |
body | array | false | ||
| location_id | 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. |
body | string | false | ||
| metrc_transfer_id | 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. |
body | integer | false | ||
| order_datetime | 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. |
body | string | false | ||
| owner_id | The ID of the Distru user that owns this purchase order. Optional; send null to leave it unassigned. | body | string | false | ||
| status | 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.COMPLETED DELIVERING PENDING PARTIALLY_RECEIVED PROCESSING |
body | string | false | ||
| supplier_location_id | 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. | body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single purchase orders | PurchaseResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Reports
Get the Cost of Goods Sold report
Success scenario
GET /public/v1/reports/cogs?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNjZhODMzMjUtMzY0Ni00MDdkLWFhYmMtNmJmOWM5MWMyY2IxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODQyIiwidHlwIjoiYWNjZXNzIn0.VZ0LMbqAy7Rz4QYrAVvQLe2TrTn7HMwqBpTXfcpPM5Y
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a599f3d309f3215473f7bf0522d181eb-d036adfce3925207-0
{
"data": [
{
"batch": "",
"cost_origin": "",
"final_input": "Final",
"margin_actual": "",
"margin_default": "",
"metrc_production_batch_number": "",
"order_number": "SO-1",
"package": "",
"product_brand": "",
"product_category": "Some category 140",
"product_name": "Alpha",
"profit_unit_actual": "",
"profit_unit_default": "",
"quantity": "4",
"sku": "SKU-A",
"total_cost_actual": "",
"total_cost_default": "",
"total_price": "40",
"total_profits_actual": "",
"total_profits_default": "",
"unit_cost_actual": "",
"unit_cost_default": "",
"unit_price": "10",
"unit_type": "Gram"
}
],
"meta": {
"columns": [
{
"key": "order_number",
"label": "Order Number"
},
{
"key": "final_input",
"label": "Final/Input"
},
{
"key": "cost_origin",
"label": "Cost Origin"
},
{
"key": "product_name",
"label": "Product Name"
},
{
"key": "sku",
"label": "SKU"
},
{
"key": "product_brand",
"label": "Product Brand"
},
{
"key": "product_category",
"label": "Product Category"
},
{
"key": "package",
"label": "Package"
},
{
"key": "metrc_production_batch_number",
"label": "Metrc Production Batch Number"
},
{
"key": "batch",
"label": "Batch"
},
{
"key": "quantity",
"label": "Quantity"
},
{
"key": "unit_type",
"label": "Unit Type"
},
{
"key": "unit_price",
"label": "Unit Price"
},
{
"key": "total_price",
"label": "Total Price"
},
{
"key": "unit_cost_actual",
"label": "Unit Cost (Actual)"
},
{
"key": "unit_cost_default",
"label": "Unit Cost (Default)"
},
{
"key": "total_cost_actual",
"label": "Total Cost (Actual)"
},
{
"key": "total_cost_default",
"label": "Total Cost (Default)"
},
{
"key": "total_profits_actual",
"label": "Total Profits (Actual)"
},
{
"key": "total_profits_default",
"label": "Total Profits (Default)"
},
{
"key": "profit_unit_actual",
"label": "Profit/Unit (Actual)"
},
{
"key": "profit_unit_default",
"label": "Profit/Unit (Default)"
},
{
"key": "margin_actual",
"label": "Margin (Actual)"
},
{
"key": "margin_default",
"label": "Margin (Default)"
}
],
"date_range": "Aug 24, 2026",
"report": "cogs"
}
}
Error scenario: invalid order date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cdc552e7865822192251f222716064cd-f4a14303dacfcb7e-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
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.
Every 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.
Filtering:
• order_datetime and delivery_datetime each narrow the set by a date range and are combined with AND when both are supplied.
• 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.
Cost 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.
Every 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.
Required permission: reports_permissions_cogs.
Request
GET /public/v1/reports/cogs
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| delivery_datetime | 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. |
query | string | false | ?delivery_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| order_datetime | 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. |
query | string | false | ?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Cost of Goods Sold report | CogsReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Cultivation Transaction History report
Success scenario
GET /public/v1/reports/cultivation-transaction-history?datetime=2000-01-01T00%3A00%3A00Z%2C2999-01-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjEsImlhdCI6MTc4NzU4NzI2MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYjRiZGVhOTAtOWI0Mi00NmMwLThlOTgtNDFiNGNmODZiMzVmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NyIsInR5cCI6ImFjY2VzcyJ9.4G9iEDKepM78rVSd99w4RKzjW28ypbgbLEEeyDKJ4HE
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 48cb83a91ca64a105eb07c7b5d1d2734-710d59e670413169-0
{
"data": [
{
"amount": "1",
"batch_name": "Plant Group 4358",
"date": "08/24/2026",
"description": null,
"package_label_s": null,
"plant_tag_s": null,
"product_name": null,
"related_entity": null,
"related_entity_status": null,
"strain": "Blue Dream",
"type": "Plant Batch Creation",
"unit": "Unit"
},
{
"amount": "1",
"batch_name": "Plant Group 1991",
"date": "08/24/2026",
"description": null,
"package_label_s": null,
"plant_tag_s": null,
"product_name": null,
"related_entity": null,
"related_entity_status": null,
"strain": "OG Kush",
"type": "Plant Batch Creation",
"unit": "Unit"
}
],
"meta": {
"columns": [
{
"key": "date",
"label": "Date"
},
{
"key": "strain",
"label": "Strain"
},
{
"key": "batch_name",
"label": "Batch Name"
},
{
"key": "plant_tag_s",
"label": "Plant Tag(s)"
},
{
"key": "product_name",
"label": "Product Name"
},
{
"key": "package_label_s",
"label": "Package Label(s)"
},
{
"key": "type",
"label": "Type"
},
{
"key": "related_entity",
"label": "Related Entity"
},
{
"key": "related_entity_status",
"label": "Related Entity Status"
},
{
"key": "amount",
"label": "Amount"
},
{
"key": "unit",
"label": "Unit"
},
{
"key": "description",
"label": "Description"
}
],
"date_range": "Dec 31, 1999 to Dec 31, 2998",
"report": "cultivation_transaction_history"
}
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4276002f86e8e97a89636f846c100b15-d5252433d344dc46-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"datetime"
],
"section": "query"
}
]
}
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.
Rows 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.
Every 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.
Required permission: reports_permissions_cultivation_transaction_history (and, for the total_cost column, permission to view costs).
Request
GET /public/v1/reports/cultivation-transaction-history
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| datetime | 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. |
query | string | false | ?datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| license_ids | 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. | query | array | false | ?license_ids[]=9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f&license_ids[]=0d2e3f4a-5b6c-7d8e-9f0a-1b2c3d4e5f6a | |
| plant_batch_ids | 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. | query | array | false | ?plant_batch_ids[]=6f8a3b1e-1c2d-4e5f-8a9b-0c1d2e3f4a5b&plant_batch_ids[]=7a9b4c2f-2d3e-5f6a-9b0c-1d2e3f4a5b6c | |
| strain | 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. |
query | string | false | ?strain=Blue Dream | |
| transaction_type | 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.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 |
query | string | false | ?transaction_type=Move Plant(s) |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Cultivation Transaction History report | CultivationTransactionHistoryReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Harvest Outputs report
Success scenario
GET /public/v1/reports/harvest-outputs
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYTY4MWI4MzYtMjgyYS00ZjAyLWEyNzQtYzc1YTBmYmZkMGE0IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODkyIiwidHlwIjoiYWNjZXNzIn0.jJ_0hvwWI4juBxkzTuYg1wDTOqWZEZj7aahw_CC43jQ
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 0682b7608ee870c032328aa514054139-3b1513868487b892-0
{
"data": [
{
"cost_input_output": "Output",
"distru_product": "Product 391",
"harvest_assembly_date": "08/24/2026",
"harvest_assembly_number": "HAS-0000001",
"harvest_name": "Spring-Hill-Kush-#17-08/24/2026",
"line_item_id": "324683e0-6f45-414b-9141-79d0d3fc9e0d",
"location": "Place 219",
"output_batch_number": null,
"output_package_number": "1A4010200001234000000011",
"output_reference_id": null,
"product_category": "Some category 171",
"quantity": "10",
"status": "PENDING",
"strain": "Spring Hill Kush #17",
"unit_type": "Gram"
},
{
"cost_input_output": "Input",
"distru_product": "Spring-Hill-Kush-#17-08/24/2026",
"harvest_assembly_date": "08/24/2026",
"harvest_assembly_number": "HAS-0000001",
"harvest_name": "Spring-Hill-Kush-#17-08/24/2026",
"line_item_id": "e07492c8-132a-482d-821a-485f3576c955",
"location": "Place 183",
"output_batch_number": null,
"output_package_number": null,
"output_reference_id": null,
"product_category": null,
"quantity": "10",
"status": "PENDING",
"strain": "Spring Hill Kush #17",
"unit_type": "Gram"
}
],
"meta": {
"columns": [
{
"key": "harvest_assembly_date",
"label": "Harvest Assembly Date"
},
{
"key": "harvest_assembly_number",
"label": "Harvest Assembly Number"
},
{
"key": "status",
"label": "Status"
},
{
"key": "harvest_name",
"label": "Harvest Name"
},
{
"key": "output_batch_number",
"label": "Output Batch Number"
},
{
"key": "output_package_number",
"label": "Output Package Number"
},
{
"key": "strain",
"label": "Strain"
},
{
"key": "location",
"label": "Location"
},
{
"key": "cost_input_output",
"label": "Cost/Input/Output"
},
{
"key": "distru_product",
"label": "Distru Product"
},
{
"key": "product_category",
"label": "Product Category"
},
{
"key": "quantity",
"label": "Quantity"
},
{
"key": "unit_type",
"label": "Unit Type"
},
{
"key": "line_item_id",
"label": "Line Item ID"
},
{
"key": "output_reference_id",
"label": "Output Reference ID"
}
],
"date_range": "Aug 17, 2026 to Aug 24, 2026",
"report": "harvest_outputs"
}
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 66f4738d9b81313322ab5eb3eddfd39d-2020adb7a1dfcbc4-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"datetime"
],
"section": "query"
}
]
}
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.
This 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.
The 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).
Every 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.
Required permission: reports_permissions_harvest_outputs.
Request
GET /public/v1/reports/harvest-outputs
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| harvest_name | Keep only assemblies that have an input whose harvest name contains this text (case-insensitive substring). Combined with the other input filters using AND. | query | string | false | ||
| location_id | 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. | query | string | false | ||
| output_product_category_id | 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. |
query | string | false | ||
| output_product_name | Keep only assemblies that have an output whose product name contains this text (case-insensitive substring). Combined with output_product_category_id using AND. |
query | string | false | ||
| status | Keep only assemblies with this status. PENDING = not yet completed; COMPLETED = completed. Omit to return assemblies of any status. SCREAMING_CASE.PENDING COMPLETED |
query | string | false | COMPLETED | |
| strain | Keep only assemblies that have an input whose harvest strain contains this text (case-insensitive substring). Combined with the other input filters using AND. | query | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Harvest Outputs report | HarvestOutputsReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Inventory Assets report
Success scenario
GET /public/v1/reports/inventory-assets
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNTAyZjQwZTMtMGM0MS00YmY1LTg0MTktODM1NTY2NTdjOTVmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NjI2IiwidHlwIjoiYWNjZXNzIn0.vBjxA3eKK7Wmm0pKefCUuPKi8KdPE_hA5DA2jyvcHMM
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 796a9f2a100f3f928249f1f272b748e3-5bab47d0428aa332-0
{
"data": [
{
"active_quantity": "100",
"assembling_quantity": "0",
"batch_number": "B1",
"category": "Some category 96",
"expiration_date": null,
"harvest_date": null,
"license": null,
"location": "L1",
"owner": "FirstName1296 LastName1297",
"package_number": null,
"product": "Widget",
"selling_quantity": "0",
"sku": "sku 230",
"subcategory": "Some subcategory 97",
"tracking_method": "BATCH",
"unit_price": "1.00",
"unit_type": "Gram",
"vendor": "Company 523"
},
{
"active_quantity": "50",
"assembling_quantity": "0",
"batch_number": "B1",
"category": "Some category 96",
"expiration_date": null,
"harvest_date": null,
"license": null,
"location": "L2",
"owner": "FirstName1296 LastName1297",
"package_number": null,
"product": "Widget",
"selling_quantity": "0",
"sku": "sku 230",
"subcategory": "Some subcategory 97",
"tracking_method": "BATCH",
"unit_price": "1.00",
"unit_type": "Gram",
"vendor": "Company 523"
}
],
"meta": {
"columns": [
{
"key": "product",
"label": "Product"
},
{
"key": "license",
"label": "License"
},
{
"key": "location",
"label": "Location"
},
{
"key": "package_number",
"label": "Package Number"
},
{
"key": "batch_number",
"label": "Batch Number"
},
{
"key": "sku",
"label": "SKU"
},
{
"key": "vendor",
"label": "Vendor"
},
{
"key": "owner",
"label": "Owner"
},
{
"key": "unit_type",
"label": "Unit Type"
},
{
"key": "active_quantity",
"label": "Active Quantity"
},
{
"key": "assembling_quantity",
"label": "Assembling Quantity"
},
{
"key": "selling_quantity",
"label": "Selling Quantity"
},
{
"key": "category",
"label": "Category"
},
{
"key": "subcategory",
"label": "Subcategory"
},
{
"key": "unit_price",
"label": "Unit Price"
},
{
"key": "expiration_date",
"label": "Expiration Date"
},
{
"key": "tracking_method",
"label": "Tracking Method"
},
{
"key": "harvest_date",
"label": "Harvest Date"
}
],
"date_range": "Aug 24, 2026 - 9:01AM",
"report": "inventory_assets"
}
}
Error scenario: invalid style filter
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c4c88576e208a46117f8e942ed77bd73-c5aa458b3c7e3a2f-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"style"
],
"section": "query"
}
]
}
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.
Quantities 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.
Pass 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.
The 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.
Every 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.
Required permission: reports_permissions_inventory_assets (granular style additionally requires permission to view cost details).
Request
GET /public/v1/reports/inventory-assets
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| datetime | 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. | query | string | false | ?datetime=2026-07-01T00:00:00Z | |
| location_id | Narrow the report to a single location by its Distru location ID. Omit to include every location the caller's company can see. | query | string | false | ?location_id=a1b2c3d4-0000-0000-0000-000000000000 | |
| style | 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.collapsed granular |
query | string | false | ?style=granular |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Inventory Assets report | InventoryAssetsReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Inventory Transaction History report
Success scenario
GET /public/v1/reports/inventory-transaction-history
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiY2UzOWI1N2QtYjhhMy00NDg5LWFkZWQtZDMzYTkwMjU5MGMwIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NzIwIiwidHlwIjoiYWNjZXNzIn0._Ks24JD4wonYWpBQ1CWLVC5K7yMwdSB5nWQnRn3M3F0
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f928fc6dd07268b16714cf27ab050a58-42e1892b5fb5ea25-0
{
"data": [
{
"amount": "100",
"batch_id": "00000000-0000-0000-0000-00000000004a",
"batch_number": null,
"cbd": null,
"cbd_mg_g": null,
"cbd_mg_ml": null,
"company_relationship_id": null,
"date": "2026-08-24T16:01:05.461291Z",
"description": "FirstName1502 LastName1503 moved 100 g of Batch B1 of Widget from gain to active in Place 129 with reason 'revaluation'",
"metrc_production_batch_number": null,
"metrc_unit_name": null,
"package_batch_number_or_batch_name": "B1",
"package_label": null,
"product": "Widget",
"product_id": "cb2cf4a2-0a8e-4b82-966c-dcb0e96fe231",
"related_entity": "Stock Adjustment",
"related_entity_customer_vendor": null,
"related_entity_status": null,
"thc": null,
"thc_mg_g": null,
"thc_mg_ml": null,
"total_cbd": null,
"total_cbd_mg_g": null,
"total_cbd_mg_ml": null,
"total_cost": null,
"total_thc": null,
"total_thc_mg_g": null,
"total_thc_mg_ml": null,
"type": "adjustment",
"unit_type": "Gram"
}
],
"meta": {
"columns": [
{
"key": "date",
"label": "Date"
},
{
"key": "product",
"label": "Product"
},
{
"key": "package_batch_number_or_batch_name",
"label": "Package Batch Number or Batch Name"
},
{
"key": "batch_number",
"label": "Batch Number"
},
{
"key": "package_label",
"label": "Package Label"
},
{
"key": "metrc_production_batch_number",
"label": "Metrc Production Batch Number"
},
{
"key": "type",
"label": "Type"
},
{
"key": "related_entity",
"label": "Related Entity"
},
{
"key": "related_entity_status",
"label": "Related Entity Status"
},
{
"key": "related_entity_customer_vendor",
"label": "Related Entity Customer/Vendor"
},
{
"key": "amount",
"label": "Amount"
},
{
"key": "unit_type",
"label": "Unit Type"
},
{
"key": "metrc_unit_name",
"label": "Metrc Unit Name"
},
{
"key": "product_id",
"label": "Product Id"
},
{
"key": "batch_id",
"label": "Batch Id"
},
{
"key": "company_relationship_id",
"label": "Company Relationship Id"
},
{
"key": "description",
"label": "Description"
},
{
"key": "thc",
"label": "THC %"
},
{
"key": "total_thc",
"label": "Total THC %"
},
{
"key": "thc_mg_g",
"label": "THC mg/g"
},
{
"key": "thc_mg_ml",
"label": "THC mg/mL"
},
{
"key": "total_thc_mg_g",
"label": "Total THC mg/g"
},
{
"key": "total_thc_mg_ml",
"label": "Total THC mg/mL"
},
{
"key": "cbd",
"label": "CBD %"
},
{
"key": "total_cbd",
"label": "Total CBD %"
},
{
"key": "cbd_mg_g",
"label": "CBD mg/g"
},
{
"key": "cbd_mg_ml",
"label": "CBD mg/mL"
},
{
"key": "total_cbd_mg_g",
"label": "Total CBD mg/g"
},
{
"key": "total_cbd_mg_ml",
"label": "Total CBD mg/mL"
},
{
"key": "total_cost",
"label": "Total Cost"
}
],
"date_range": "Jul 25, 2026 to Aug 24, 2026",
"report": "inventory_transaction_history"
}
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: d0847ed605160db0953001734c1d4a02-272c7b3ff49d681f-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"datetime"
],
"section": "query"
}
]
}
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.
This 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).
When 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.
Every 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.
Required permission: reports_permissions_inventory_transaction_history.
Request
GET /public/v1/reports/inventory-transaction-history
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_ids | 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. |
query | array | false | ?batch_ids[]=3fa85f64-5717-4562-b3fc-2c963f66afa6 | |
| datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| package_id | 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. |
query | string | false | ?package_id=3fa85f64-5717-4562-b3fc-2c963f66afa6 | |
| product_ids | 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. |
query | array | false | ?product_ids[]=3fa85f64-5717-4562-b3fc-2c963f66afa6 |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Inventory Transaction History report | InventoryTransactionHistoryReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Inventory Valuation report
Success scenario
GET /public/v1/reports/inventory-valuation
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmQ1MDEzZjQtYTIxNS00MWZmLTkxM2QtYTU3ODk4ZDg3OTI2IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTc1IiwidHlwIjoiYWNjZXNzIn0.t-DmXVEw9Y1nHbD13IDCod19LJCN4eEUuMoC8p9MDJE
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 5b1c65f19d94f3fa2e21f39abe992dfb-691b756aae3e90ca-0
{
"data": [
{
"active_quantity": "5.00",
"active_value_price": "50.00",
"assembling_quantity": "0.00",
"available_quantity": "5.00",
"brand": null,
"category": "Some category 24",
"group": "Product Group 30",
"image_url": null,
"incoming_quantity": "0.00",
"inventory_threshold_max": null,
"inventory_threshold_min": null,
"name": "Alpha",
"owner": "FirstName368 LastName369",
"pending_output_quantity": "0.00",
"reserved_quantity": "0.00",
"sku": "sku 60",
"subcategory": "Some subcategory 26",
"unit_cost": "4.00",
"unit_price": "10.00",
"unit_type": "Gram",
"vendor": "Company 151"
},
{
"active_quantity": "0.00",
"active_value_price": "0.00",
"assembling_quantity": "0.00",
"available_quantity": "0.00",
"brand": null,
"category": "Some category 27",
"group": "Product Group 32",
"image_url": null,
"incoming_quantity": "0.00",
"inventory_threshold_max": null,
"inventory_threshold_min": null,
"name": "Beta",
"owner": "FirstName368 LastName369",
"pending_output_quantity": "0.00",
"reserved_quantity": "0.00",
"sku": "sku 66",
"subcategory": "Some subcategory 29",
"unit_cost": "4.00",
"unit_price": "10.00",
"unit_type": "Gram",
"vendor": "Company 156"
}
],
"meta": {
"columns": [
{
"key": "name",
"label": "Name"
},
{
"key": "sku",
"label": "SKU"
},
{
"key": "vendor",
"label": "Vendor"
},
{
"key": "unit_type",
"label": "Unit Type"
},
{
"key": "category",
"label": "Category"
},
{
"key": "subcategory",
"label": "Subcategory"
},
{
"key": "group",
"label": "Group"
},
{
"key": "owner",
"label": "Owner"
},
{
"key": "active_quantity",
"label": "Active Quantity"
},
{
"key": "assembling_quantity",
"label": "Assembling Quantity"
},
{
"key": "reserved_quantity",
"label": "Reserved Quantity"
},
{
"key": "available_quantity",
"label": "Available Quantity"
},
{
"key": "incoming_quantity",
"label": "Incoming Quantity"
},
{
"key": "pending_output_quantity",
"label": "Pending Output Quantity"
},
{
"key": "unit_cost",
"label": "Unit Cost"
},
{
"key": "unit_price",
"label": "Unit Price"
},
{
"key": "inventory_threshold_max",
"label": "Inventory Threshold Max"
},
{
"key": "inventory_threshold_min",
"label": "Inventory Threshold Min"
},
{
"key": "active_value_price",
"label": "Active Value (Price)"
},
{
"key": "image_url",
"label": "Image URL"
},
{
"key": "brand",
"label": "Brand"
}
],
"date_range": "Aug 24, 2026",
"report": "inventory_valuation"
}
}
Error scenario: invalid calculation method
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ed6b6bbbdd567a37e7e6bb95a311745b-ae0970a744d62a3f-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"calculation_method"
],
"section": "query"
}
]
}
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.
Quantities are derived, not stored verbatim:
• available_quantity is active_quantity minus reserved_quantity, and can be negative when reservations exceed on-hand stock.
• incoming_quantity sums quantities on open (not-yet-received) purchases; pending_output_quantity sums the output still owed by open assemblies.
• 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).
The 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).
Every 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.
Required permission: reports_permissions_inventory_valuation.
Request
GET /public/v1/reports/inventory-valuation
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| brand_ids | Keeps only products under these brands. Values are Distru brand IDs (the brand company relationship). Omit (or pass empty) to include every brand. | query | array | false | ?brand_ids[]=7d0c3b98-2a4e-4c61-9f83-5b0d1e6a7c29 | |
| calculation_method | 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.cost price |
query | string | false | ?calculation_method=cost | |
| location_ids | 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. |
query | array | false | ?location_ids[]=b6f1e4c2-8d3a-4a1e-9c77-2f0e5d1a4b39&location_ids[]=9a2d7f10-1c6b-4e5f-8a90-3d7c2e1b0a44 | |
| search | 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. | query | string | false | ?search=Blue%20Dream | |
| user_ids | 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. |
query | array | false | ?user_ids[]=3c8b0a51-7d2e-4f80-b1a6-9e0c4d5f2a13 | |
| vendor_ids | Keeps only products supplied by these vendors. Values are Distru vendor IDs (the vendor company relationship). Omit (or pass empty) to include every vendor. | query | array | false | ?vendor_ids[]=1f4e9c22-6b3d-4a70-8e15-0c9d7f2a3b58 |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Inventory Valuation report | InventoryValuationReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Invoice History report
Success scenario
GET /public/v1/reports/invoice-history?invoice_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjEsImlhdCI6MTc4NzU4NzI2MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYTVlMGZiMDMtYWIxNC00MmQzLThmNjAtY2ViNjRkYTg5YzljIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTYiLCJ0eXAiOiJhY2Nlc3MifQ.sfudF_N0YUzOS_U6R88qnkTx9isAi3c4NxQpcTeLOU0
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 067bac4681c5fe340374e50cfab4a626-6fb7c4349d97b174-0
{
"data": [
{
"charge_summary": null,
"customer": "Company 27",
"discount_summary": null,
"due_date": "2026-08-24",
"invoice_date": "2026-07-01",
"invoice_number": "INV-2",
"line_item_subtotal": "0.00",
"outstanding": "500.00",
"owner": "FirstName74 LastName75",
"paid": "0.00",
"sales_order": "SO-2",
"status": "NOT_PAID",
"tax_summary": null,
"total": "500.00",
"total_charges": "0.00",
"total_discounts": "0.00",
"total_taxes": "0.00"
},
{
"charge_summary": null,
"customer": "Company 20",
"discount_summary": null,
"due_date": "2026-08-24",
"invoice_date": "2026-07-01",
"invoice_number": "INV-1",
"line_item_subtotal": "0.00",
"outstanding": "1000.00",
"owner": "FirstName38 LastName39",
"paid": "0.00",
"sales_order": "SO-0",
"status": "FULLY_PAID",
"tax_summary": null,
"total": "1000.00",
"total_charges": "0.00",
"total_discounts": "0.00",
"total_taxes": "0.00"
}
],
"meta": {
"columns": [
{
"key": "invoice_date",
"label": "Invoice Date"
},
{
"key": "invoice_number",
"label": "Invoice Number"
},
{
"key": "due_date",
"label": "Due Date"
},
{
"key": "sales_order",
"label": "Sales Order"
},
{
"key": "customer",
"label": "Customer"
},
{
"key": "status",
"label": "Status"
},
{
"key": "paid",
"label": "Paid"
},
{
"key": "outstanding",
"label": "Outstanding"
},
{
"key": "line_item_subtotal",
"label": "Line Item Subtotal"
},
{
"key": "total_taxes",
"label": "Total Taxes"
},
{
"key": "tax_summary",
"label": "Tax Summary"
},
{
"key": "total_charges",
"label": "Total Charges"
},
{
"key": "charge_summary",
"label": "Charge Summary"
},
{
"key": "total_discounts",
"label": "Total Discounts"
},
{
"key": "discount_summary",
"label": "Discount Summary"
},
{
"key": "total",
"label": "Total"
},
{
"key": "owner",
"label": "Owner"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "invoice_history"
}
}
Error scenario: invalid invoice date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 6b64f8f447715e10cbaee9ce8b50e559-cea913f4ef0dcf4b-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"invoice_datetime"
],
"section": "query"
}
]
}
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.
Scope and date defaulting:
• Results are scoped to the company that owns the API key, and further to the invoices the key's user is allowed to view.
• 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.
• The resolved invoice-date window (whether you passed it or it defaulted) is echoed back as a human-readable string in meta.date_range.
Row shape and value formatting:
• 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.
• 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.
• 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.
• status is a SCREAMING_CASE payment-status token (see the values below).
Extra columns:
• 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. • Any Invoice custom fields configured for the company are appended as additional columns, keyed by a slug of the field label.
Because 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.
Required permission: view the invoice history report.
Request
GET /public/v1/reports/invoice-history
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_ids | 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). | query | array | false | ?batch_ids[]=b3d1c2e4-... | |
| company_relationship_ids | 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). | query | array | false | ?company_relationship_ids[]=b3d1c2e4-...&company_relationship_ids[]=a1f2... | |
| due_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| invoice_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| order_status | 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. PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
query | array | false | ?order_status[]=COMPLETED&order_status[]=DELIVERED | |
| paid | 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. |
query | string | false | 0,100 | |
| product_ids | 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). | query | array | false | ?product_ids[]=8f2c9a10-4b7e-4d3a-9c1f-2e5b6a7d8c90 | |
| search | 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. | query | string | false | INV-1024 | |
| shipped_from_license_ids | Filter by the license the order shipped from. Each value is a Distru license id. Repeat the key to pass several; matches any (OR). | query | array | false | ?shipped_from_license_ids[]=b3d1c2e4-... | |
| status | 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. NOT_PAID PARTIALLY_PAID FULLY_PAID OVER_PAID |
query | array | false | ?status[]=FULLY_PAID&status[]=PARTIALLY_PAID | |
| total | Filter by the invoice grand total, as a comma-separated min,max numeric range. Bounds are inclusive; omit a side to leave it unbounded. |
query | string | false | 100,500 |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Invoice History report | InvoiceHistoryReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Order Fulfillment report
Success scenario
GET /public/v1/reports/order-fulfillment?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYzY0N2UwZDctZGE5OS00YWZiLTg0MWItYWU5NDVhMjQwMDA3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NTYiLCJ0eXAiOiJhY2Nlc3MifQ.Rc2Kk4uRIc5JNyTO5NhicMxt1-A62EfH8LnbthjYm4M
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b45f06c50ef2488fe8782761a1a97432-13bd671d707b51ac-0
{
"data": [
{
"category": "Some category 8",
"group": "Product Group 8",
"product": "A1",
"so_1": "3",
"so_2": "",
"subcategory": "Some subcategory 8",
"total_units": "3",
"total_value": "30.00",
"unit_price": "10.00"
},
{
"category": "Some category 10",
"group": "Product Group 9",
"product": "B2",
"so_1": "1",
"so_2": "4",
"subcategory": "Some subcategory 9",
"total_units": "5",
"total_value": "100.00",
"unit_price": "20.00"
}
],
"meta": {
"columns": [
{
"key": "product",
"label": "Product"
},
{
"key": "group",
"label": "Group"
},
{
"key": "category",
"label": "Category"
},
{
"key": "subcategory",
"label": "Subcategory"
},
{
"key": "so_1",
"label": "SO-1"
},
{
"key": "so_2",
"label": "SO-2"
},
{
"key": "total_units",
"label": "Total Units"
},
{
"key": "unit_price",
"label": "Unit Price"
},
{
"key": "total_value",
"label": "Total Value"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "order_fulfillment"
}
}
Error scenario: invalid order date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: bf55b42f419e341dd04d16415ef9a169-fda89e938634ecae-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
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.
When 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.
Every 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.
Required permission: reports_permissions_order_fulfillment.
Request
GET /public/v1/reports/order-fulfillment
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| company_relationship_ids | 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. | query | array | false | ?company_relationship_ids[]=6f0e8c6e-3b1a-4c2d-9f77-1a2b3c4d5e6f | |
| location_ids | 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. |
query | array | false | ?location_ids[]=aa11bb22-cc33-dd44-ee55-ff6677889900 | |
| order_datetime | 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. |
query | string | false | ?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| owner_ids | 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. | query | array | false | ?owner_ids[]=99887766-5544-3322-1100-ffeeddccbbaa | |
| product_ids | Restricts the report to these products, given as product IDs. Omit to include every product sold in the range. | query | array | false | ?product_ids[]=3f2504e0-4f89-41d3-9a0c-0305e82c3301 | |
| search | 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. | query | string | false | ||
| status | 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. PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
query | array | false | ?status[]=COMPLETED&status[]=DELIVERED | |
| user_ids | 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. |
query | array | false | ?user_ids[]=11223344-5566-7788-99aa-bbccddeeff00 |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Order Fulfillment report | OrderFulfillmentReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Plant Lifecycle report
Success scenario
GET /public/v1/reports/plant-lifecycle?datetime=2000-01-01T00%3A00%3A00Z%2C2999-01-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjEsImlhdCI6MTc4NzU4NzI2MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZDljOTE1ZDItY2ViNy00YTg0LTg3NWYtYjk3OTFiMjgzZjQ5IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MSIsInR5cCI6ImFjY2VzcyJ9.zUhJLUPgGw_YL-WxyMvSHFJ89FneSyFVTd-n5EArlaE
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c3abfe0ae543d18f28732cdcab968875-5b3165eda9697d3f-0
{
"data": [
{
"batch_creation_date": "08/24/2026",
"days_as_batch": "0",
"days_veg_to_last_harvest": null,
"first_harvest_date": null,
"harvest_name_s": null,
"last_harvest_date": null,
"plant_batch_name": "Plant Group 1292",
"plants_destroyed": "0",
"plants_harvested": "0",
"plants_promoted_to_veg": "0",
"plants_started": "3",
"promoted_to_veg_date": null,
"strain": "OG Kush",
"total_lifecycle_days": "0"
},
{
"batch_creation_date": "08/24/2026",
"days_as_batch": "0",
"days_veg_to_last_harvest": null,
"first_harvest_date": null,
"harvest_name_s": null,
"last_harvest_date": null,
"plant_batch_name": "Plant Group 4294",
"plants_destroyed": "0",
"plants_harvested": "0",
"plants_promoted_to_veg": "0",
"plants_started": "2",
"promoted_to_veg_date": null,
"strain": "Blue Dream",
"total_lifecycle_days": "0"
}
],
"meta": {
"columns": [
{
"key": "plant_batch_name",
"label": "Plant Batch Name"
},
{
"key": "batch_creation_date",
"label": "Batch Creation Date"
},
{
"key": "strain",
"label": "Strain"
},
{
"key": "plants_started",
"label": "Plants Started"
},
{
"key": "plants_promoted_to_veg",
"label": "Plants Promoted to Veg"
},
{
"key": "plants_destroyed",
"label": "Plants Destroyed"
},
{
"key": "plants_harvested",
"label": "Plants Harvested"
},
{
"key": "promoted_to_veg_date",
"label": "Promoted to Veg Date"
},
{
"key": "first_harvest_date",
"label": "First Harvest Date"
},
{
"key": "last_harvest_date",
"label": "Last Harvest Date"
},
{
"key": "days_as_batch",
"label": "Days as Batch"
},
{
"key": "days_veg_to_last_harvest",
"label": "Days Veg to Last Harvest"
},
{
"key": "total_lifecycle_days",
"label": "Total Lifecycle Days"
},
{
"key": "harvest_name_s",
"label": "Harvest Name(s)"
}
],
"date_range": "Dec 31, 1999 to Dec 31, 2998",
"report": "plant_lifecycle"
}
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 1711e57ec1288dd6dafef7cf06b6bbb7-e4ff19af662803db-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"datetime"
],
"section": "query"
}
]
}
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.
Scope 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.
Date 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).
Reading 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.
Cost 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).
Report-level information (the resolved human-readable date range and the ordered column definitions) is returned under meta.
Required permission: reports_permissions_plant_lifecycle.
Request
GET /public/v1/reports/plant-lifecycle
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| datetime | 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. |
query | string | false | ?datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| strain | 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. | query | string | false | ?strain=Blue |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Plant Lifecycle report | PlantLifecycleReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Purchase Order History report
Success scenario
GET /public/v1/reports/purchase-order-history?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMjEwYTY2MDUtZTU0NS00YTUwLTliNjMtOTFhOGIwMTNmMjlhIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6OTk3IiwidHlwIjoiYWNjZXNzIn0.pCxKcRtpJ3Xdn81gpdEOIxrW3Ir1hUwwekM4Ew_5fAQ
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cb9d92070a9f880137856c848a6ff10a-2174574e603a4ec4-0
{
"data": [
{
"amount": "500.00",
"due_date": "2026-08-24T09:01:06.494131",
"owner": "FirstName2054 LastName2055",
"paid": "0.0",
"purchase_date": "2026-07-01T05:00:00.000000",
"purchase_number": "PO-2",
"status": "PENDING",
"vendor": "Company 804"
},
{
"amount": "1000.00",
"due_date": "2026-08-24T09:01:06.466449",
"owner": "FirstName2038 LastName2039",
"paid": "0.0",
"purchase_date": "2026-07-01T05:00:00.000000",
"purchase_number": "PO-1",
"status": "COMPLETED",
"vendor": "Company 795"
}
],
"meta": {
"columns": [
{
"key": "purchase_date",
"label": "Purchase Date"
},
{
"key": "due_date",
"label": "Due Date"
},
{
"key": "purchase_number",
"label": "Purchase Number"
},
{
"key": "vendor",
"label": "Vendor"
},
{
"key": "status",
"label": "Status"
},
{
"key": "paid",
"label": "Paid"
},
{
"key": "amount",
"label": "Amount"
},
{
"key": "owner",
"label": "Owner"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "purchase_order_history"
}
}
Error scenario: invalid order_datetime range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: df81fa410f6f879307788595890cb356-a36cdb7646b50e11-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
Read-only report that returns one row per 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.
Scope and filtering:
• Only purchases visible to the API key's company are included; there are no cross-company rows.
• 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.
• 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.
• 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.
Row shape:
• 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.
• 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.
• meta also returns the resolved human-readable date range the report covers (after the 30-day default is applied).
Required permission: reports_permissions_purchase_order_history.
Request
GET /public/v1/reports/purchase-order-history
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_ids | Keep only purchases that include one of the given batches, by Distru batch ID (OR within the list). | query | array | false | ?batch_ids[]= |
|
| company_relationship_ids | Keep only purchases from the given vendors, identified by Distru company-relationship ID (OR within the list). Omit to include every vendor. | query | array | false | ?company_relationship_ids[]= |
|
| created_datetime | 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. |
query | string | false | ?created_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| creator_ids | Keep only purchases created by the given users, by Distru user ID (OR within the list). Omit to include every creator. | query | array | false | ?creator_ids[]= |
|
| due_datetime | 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. |
query | string | false | ?due_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| location_ids | Keep only purchases received into the given warehouse locations, by Distru location ID (OR within the list). Omit to include every location. | query | array | false | ?location_ids[]= |
|
| order_datetime | 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. |
query | string | false | ?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| owner_ids | Keep only purchases owned by the given users, by Distru user ID (OR within the list). Omit to include every owner. | query | array | false | ?owner_ids[]= |
|
| paid | 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. |
query | string | false | ?paid=0,250 | |
| product_ids | Keep only purchases that include one of the given products, by Distru product ID (OR within the list). | query | array | false | ?product_ids[]= |
|
| search | 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. | query | string | false | ?search=PO-1024 | |
| status | 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. COMPLETED DELIVERING PENDING PARTIALLY_RECEIVED PROCESSING |
query | array | false | ?status[]=COMPLETED&status[]=DELIVERING | |
| total | 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. |
query | string | false | ?total=100,500 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Purchase Order History report | PurchaseOrderHistoryReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Purchases By Company report
Success scenario
GET /public/v1/reports/purchases-by-company?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjEsImlhdCI6MTc4NzU4NzI2MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMzM5MTRlOGUtNTRjMy00ZmZlLWIxOTAtZDBiMzQ2ZjdkOGRkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTAiLCJ0eXAiOiJhY2Nlc3MifQ.TghDrp8yOXrmAQwC6jEy3O-7RTwX3_J6S0_dvaoua2c
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 17d81f2a7deb50f9ae5f82a8c7361d54-9930d774b5f57663-0
{
"data": [
{
"category": "Manufacturer",
"last_purchase_date": "7/15/2026",
"name": "Alpha",
"product_owner": "FirstName46 LastName47",
"purchase_order_count": "2",
"relationship_type": null,
"total_purchases": "1500.00"
}
],
"meta": {
"columns": [
{
"key": "name",
"label": "Name"
},
{
"key": "last_purchase_date",
"label": "Last Purchase Date"
},
{
"key": "purchase_order_count",
"label": "Purchase Order Count"
},
{
"key": "total_purchases",
"label": "Total Purchases"
},
{
"key": "product_owner",
"label": "Product Owner"
},
{
"key": "category",
"label": "Category"
},
{
"key": "relationship_type",
"label": "Relationship Type"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "purchases_by_company"
}
}
Error scenario: invalid order_datetime range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: fd8ae604b43a1e995735af261e8e2ad8-4640f26cc6a6bdaf-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
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.
Which vendors appear:
• 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.
• Draft purchases are never counted, and purchases you lack permission to view (team visibility) are excluded from the counts and totals.
Behaviors worth noting:
• 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.
• purchase_order_count and total_purchases are computed only over the purchases inside the date range (and matching the owner_ids filter).
• When no date filter is provided, the report defaults to the last 30 days. All date bounds are interpreted in your company's timezone.
Every 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.
Required permission: reports_permissions_purchases_by_company.
Request
GET /public/v1/reports/purchases-by-company
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| company_relationship_group_ids | 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. | query | array | false | ?company_relationship_group_ids[]= |
|
| order_datetime | 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. |
query | string | false | ?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]= |
|
| search | 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. | query | string | false | ?search=green |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Purchases By Company report | PurchasesByCompanyReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Purchases By Product report
Success scenario
GET /public/v1/reports/purchases-by-product?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiODA4M2M1YjYtNTEwNy00MTA4LWExZTgtZDBlMGY3MzczMDJkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NDYzIiwidHlwIjoiYWNjZXNzIn0.l-tuqFEhfRowcQxhma-GmcfIvohF4azvOhtsAJ5y4ZY
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: dac94633c5b26042347b159dce247fd9-dd378e13df4d561f-0
{
"data": [
{
"category": "Some category 71",
"group": "Product Group 74",
"name": "Alpha",
"owner": "FirstName968 LastName969",
"quantity_purchased": "4",
"sale_price": "1.00",
"sku": "sku 170",
"subcategory": "Some subcategory 72",
"total_purchased": "40.00",
"unit_cost": null,
"unit_type": "Gram",
"vendor": "Company 398",
"wholesale_price": null
}
],
"meta": {
"columns": [
{
"key": "name",
"label": "Name"
},
{
"key": "sku",
"label": "SKU"
},
{
"key": "quantity_purchased",
"label": "Quantity Purchased"
},
{
"key": "total_purchased",
"label": "Total Purchased"
},
{
"key": "unit_type",
"label": "Unit Type"
},
{
"key": "category",
"label": "Category"
},
{
"key": "subcategory",
"label": "Subcategory"
},
{
"key": "group",
"label": "Group"
},
{
"key": "vendor",
"label": "Vendor"
},
{
"key": "owner",
"label": "Owner"
},
{
"key": "unit_cost",
"label": "Unit Cost"
},
{
"key": "sale_price",
"label": "Sale Price"
},
{
"key": "wholesale_price",
"label": "Wholesale Price"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "purchases_by_product"
}
}
Error scenario: invalid order_datetime range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f8db4c4571da0485a78162fec59c4bdd-bd31a2841fa09826-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
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.
This 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).
Date 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.
Every 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.
Required permission: reports_permissions_purchases_by_product.
Request
GET /public/v1/reports/purchases-by-product
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| location_ids | 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. | query | array | false | ?location_ids[]=3b12f1df-8f7a-4e2b-9c1a-2d3e4f5a6b7c | |
| order_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=8f14e45f-cea1-4b2e-9a1d-0c2f3a4b5c6d | |
| search | Keep only products whose name or SKU matches this text. Omit to include all products. | query | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Purchases By Product report | PurchasesByProductReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Sales By Company report
Success scenario
GET /public/v1/reports/sales-by-company?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjEsImlhdCI6MTc4NzU4NzI2MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMGQzNGViZDQtZDM2Mi00YWFhLTg4MmUtYzM4OGRhMjRjZTA0IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NiIsInR5cCI6ImFjY2VzcyJ9.D5A8bhqKOJB1U5SaC4ctQ2nze_Yc7AmRTIPCUtGZ4Ts
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 8643b6f2cc7482cb831e3ee90e531bfa-974b72b441cbc6cb-0
{
"data": [
{
"category": "Distributor",
"last_order_date": "7/15/2026",
"name": "Alpha",
"order_count": "3",
"owner": "FirstName182 LastName183",
"relationship_type": null,
"total_received": "0.00",
"total_sales": "1800.00"
}
],
"meta": {
"columns": [
{
"key": "name",
"label": "Name"
},
{
"key": "last_order_date",
"label": "Last Order Date"
},
{
"key": "order_count",
"label": "Order Count"
},
{
"key": "total_received",
"label": "Total Received"
},
{
"key": "total_sales",
"label": "Total Sales"
},
{
"key": "owner",
"label": "Owner"
},
{
"key": "category",
"label": "Category"
},
{
"key": "relationship_type",
"label": "Relationship Type"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "sales_by_company"
}
}
Error scenario: invalid order_datetime range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 8bdda5e3e91135180abc5011c030bfab-4f156810257cfc11-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
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.
This 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.
Date handling:
• 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).
• The range and all dates are resolved in the API user's own timezone, so day boundaries follow that timezone rather than UTC.
Status handling:
• When status is omitted, every status except CANCELED is counted.
• Canceled orders are only counted when the status filter explicitly includes CANCELED.
Row ordering:
• The report does not accept a sort parameter. By default rows are returned by total_sales descending (highest-selling customer first).
• When search is supplied, rows are instead ordered by how closely the customer's name matches the search term.
Every 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.
Requires the "view the sales by company report" permission on the API key.
Request
GET /public/v1/reports/sales-by-company
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| company_relationship_group_ids | 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. |
query | array | false | ?company_relationship_group_ids[]=b1a2c3d4&company_relationship_group_ids[]=e5f6a7b8 | |
| order_datetime | 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. |
query | string | false | ?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| owner_ids | 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. | query | array | false | ?owner_ids[]=b1a2c3d4&owner_ids[]=e5f6a7b8 | |
| search | 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. | query | string | false | ||
| status | 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.PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
query | array | false | ?status[]=COMPLETED&status[]=DELIVERED |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Sales By Company report | SalesByCompanyReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Sales By Product report
Success scenario
GET /public/v1/reports/sales-by-product?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjEsImlhdCI6MTc4NzU4NzI2MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiM2YwN2Y2MzMtYjViMi00M2JhLWJmM2EtODc0YWRlZDdiMWQxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTIiLCJ0eXAiOiJhY2Nlc3MifQ.PHSZmU-JKtGtMuKyG0nkl2lSKkdC-ZCbSktxDLiDz6s
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 77d7089270641d133e8d9b7813b9cd0c-43d79ab3d29a15ae-0
{
"data": [
{
"category": "Some category 5",
"group": "Product Group 5",
"name": "Beta",
"product_owner": "FirstName78 LastName79",
"quantity_sold": "3",
"sale_price": "1.00",
"shipped_from_license": null,
"sku": "sku 13",
"subcategory": "Some subcategory 5",
"total_sales": "60.00",
"unit_cost": null,
"unit_type": "Gram",
"upc": null,
"vendor": "Company 29",
"wholesale_price": null
},
{
"category": "Some category 2",
"group": "Product Group 1",
"name": "Alpha",
"product_owner": "FirstName52 LastName55",
"quantity_sold": "4",
"sale_price": "1.00",
"shipped_from_license": null,
"sku": "sku 3",
"subcategory": "Some subcategory 1",
"total_sales": "40.00",
"unit_cost": null,
"unit_type": "Gram",
"upc": null,
"vendor": "Company 23",
"wholesale_price": null
}
],
"meta": {
"columns": [
{
"key": "name",
"label": "Name"
},
{
"key": "sku",
"label": "SKU"
},
{
"key": "quantity_sold",
"label": "Quantity Sold"
},
{
"key": "total_sales",
"label": "Total Sales"
},
{
"key": "unit_type",
"label": "Unit Type"
},
{
"key": "category",
"label": "Category"
},
{
"key": "subcategory",
"label": "Subcategory"
},
{
"key": "group",
"label": "Group"
},
{
"key": "vendor",
"label": "Vendor"
},
{
"key": "product_owner",
"label": "Product Owner"
},
{
"key": "unit_cost",
"label": "Unit Cost"
},
{
"key": "sale_price",
"label": "Sale Price"
},
{
"key": "wholesale_price",
"label": "Wholesale Price"
},
{
"key": "shipped_from_license",
"label": "Shipped From License"
},
{
"key": "upc",
"label": "UPC"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "sales_by_product"
}
}
Error scenario: invalid order_datetime range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 7728482ccc720005f42375d37e9bda37-7559b92cf4e343ed-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
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.
Use 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.
Date 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.
Response 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.
Requires the "view the sales by product report" permission.
Request
GET /public/v1/reports/sales-by-product
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| customer_ids | 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. |
query | array | false | ?customer_ids[]=b1a1c3e4-0000-0000-0000-000000000000 | |
| exclude_customer_ids | 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. |
query | array | false | ?exclude_customer_ids[]=b1a1c3e4-0000-0000-0000-000000000000 | |
| location_ids | 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. |
query | array | false | ?location_ids[]=d3c3e5a6-0000-0000-0000-000000000000 | |
| order_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| owner_ids | 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). | query | array | false | ?owner_ids[]=c2b2d4f5-0000-0000-0000-000000000000 | |
| search | Case-insensitive substring match against the product name or SKU; only products matching are included in the report. | query | string | false | ?search=blue+dream | |
| shipped_from_license_ids | Include only order items shipped from these licenses, given as public license IDs. Values combine with OR. | query | array | false | ?shipped_from_license_ids[]=f5e5a7c8-0000-0000-0000-000000000000 | |
| status | 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. PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
query | array | false | ?status[]=COMPLETED&status[]=DELIVERED | |
| user_ids | 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. |
query | array | false | ?user_ids[]=e4d4f6b7-0000-0000-0000-000000000000 |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Sales By Product report | SalesByProductReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Sales By User report
Success scenario
GET /public/v1/reports/sales-by-user?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z&user_ids[]=00000000-0000-0000-0000-0000000000c2&user_ids[]=00000000-0000-0000-0000-0000000000c7
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTFkY2IyOGQtOGUwYS00YjkxLWE3MTItMWQ1YTdjZDdlYTFlIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTkyIiwidHlwIjoiYWNjZXNzIn0.R0HCto0glXrFXwa0nYAA2qZe-TK1DmklWQfKrQAJCmk
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 04dbd14292f5ef7305410a7a15b26e3d-b54896cb2937712e-0
{
"data": [
{
"leaderboard_rank": "1",
"order_count": "2",
"sales_pre_tax": "50.00",
"total_sales": "150.00",
"user": "Alice Rep"
},
{
"leaderboard_rank": "2",
"order_count": "1",
"sales_pre_tax": "30.00",
"total_sales": "30.00",
"user": "Bob Rep"
}
],
"meta": {
"columns": [
{
"key": "leaderboard_rank",
"label": "Leaderboard Rank"
},
{
"key": "user",
"label": "User"
},
{
"key": "order_count",
"label": "Order Count"
},
{
"key": "sales_pre_tax",
"label": "Sales (Pre-Tax)"
},
{
"key": "total_sales",
"label": "Total Sales"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "sales_by_user"
}
}
Error scenario: invalid order_datetime range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 835d03a3fd71fb21c4af6395ee950b3d-8c96298aa001a288-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
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.
This 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.
Scope of what is counted:
• 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.
• 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.
• 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.
Response 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.
Requires the "view the sales by user report" permission on the API key's user.
Request
GET /public/v1/reports/sales-by-user
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| order_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| search | 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. | query | string | false | ||
| status | 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. PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
query | array | false | ?status[]=COMPLETED&status[]=DELIVERED | |
| user_ids | 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. | query | array | false | ?user_ids[]=b7f41997-f805-412c-99f7-640c6100e161 |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Sales By User report | SalesByUserReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Sales Order History report
Success scenario
GET /public/v1/reports/sales-order-history?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMDhmZmU3NDYtNjg1Ny00ZjBlLWJiODMtODE3YTY2Y2NjMDY5IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODc2IiwidHlwIjoiYWNjZXNzIn0.h5YMGheTy7hc4nDZSiEnffw9jS-DNBSPIVnts47_kvg
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ae90bdbb279409e3459e1d7ba539ae3a-144db04a17ea6551-0
{
"data": [
{
"charges_taxes_not_included": "0.00",
"customer": "Company 715",
"delivery_date": null,
"delivery_date_utc": null,
"discounts_taxes_not_included": "0.00",
"due_date": "2026-08-24T09:01:06.049537",
"due_date_utc": "2026-08-24T16:01:06.049537Z",
"order_date": "2026-07-01T05:00:00.000000",
"order_date_utc": "2026-07-01T12:00:00.000000Z",
"order_number": "SO-1",
"outstanding": "1000.00",
"owner": null,
"paid": "0.00",
"returns": "0.00",
"status": "COMPLETED",
"subtotal": "0.00",
"taxes": "0.00",
"total": "1000.00"
},
{
"charges_taxes_not_included": "0.00",
"customer": "Company 721",
"delivery_date": null,
"delivery_date_utc": null,
"discounts_taxes_not_included": "0.00",
"due_date": "2026-08-24T09:01:06.073117",
"due_date_utc": "2026-08-24T16:01:06.073117Z",
"order_date": "2026-07-01T05:00:00.000000",
"order_date_utc": "2026-07-01T12:00:00.000000Z",
"order_number": "SO-2",
"outstanding": "500.00",
"owner": null,
"paid": "0.00",
"returns": "0.00",
"status": "PENDING",
"subtotal": "0.00",
"taxes": "0.00",
"total": "500.00"
}
],
"meta": {
"columns": [
{
"key": "order_date",
"label": "Order Date"
},
{
"key": "order_date_utc",
"label": "Order Date (UTC)"
},
{
"key": "delivery_date",
"label": "Delivery Date"
},
{
"key": "delivery_date_utc",
"label": "Delivery Date (UTC)"
},
{
"key": "due_date",
"label": "Due Date"
},
{
"key": "due_date_utc",
"label": "Due Date (UTC)"
},
{
"key": "order_number",
"label": "Order Number"
},
{
"key": "customer",
"label": "Customer"
},
{
"key": "status",
"label": "Status"
},
{
"key": "paid",
"label": "Paid"
},
{
"key": "outstanding",
"label": "Outstanding"
},
{
"key": "subtotal",
"label": "Subtotal"
},
{
"key": "taxes",
"label": "Taxes"
},
{
"key": "discounts_taxes_not_included",
"label": "Discounts (taxes not included)"
},
{
"key": "charges_taxes_not_included",
"label": "Charges (taxes not included)"
},
{
"key": "returns",
"label": "Returns"
},
{
"key": "total",
"label": "Total"
},
{
"key": "owner",
"label": "Owner"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "sales_order_history"
}
}
Error scenario: invalid order_datetime range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 9edfa37fbec8b24b7fa86c3cbd8b39a1-38d1792eb1885c69-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
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).
When 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.
This 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.
Every 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.
Companies 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.
Requires the "view the sales order history report" permission on the API key.
Request
GET /public/v1/reports/sales-order-history
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_ids | Filter to orders containing any of these batches, by batch ID (OR). | query | array | false | ?batch_ids[]=8f6c...&batch_ids[]=a12b... | |
| brand_ids | Filter to orders containing a product of any of these brands, by brand ID (OR). | query | array | false | ?brand_ids[]=8f6c...&brand_ids[]=a12b... | |
| company_relationship_group_ids | Filter to orders whose customer belongs to any of these customer groups, by group ID (OR). | query | array | false | ?company_relationship_group_ids[]=8f6c...&company_relationship_group_ids[]=a12b... | |
| company_relationship_ids | Filter to orders for any of these customers, identified by their company relationship IDs (OR). | query | array | false | ?company_relationship_ids[]=8f6c...&company_relationship_ids[]=a12b... | |
| created_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| creator_ids | Filter to orders created by any of these users, by user ID (OR). | query | array | false | ?creator_ids[]=8f6c...&creator_ids[]=a12b... | |
| delivery_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| due_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| matched_with_compliance_transfer | 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. |
query | boolean | false | true | |
| menu_ids | Filter to orders placed against any of these menus, by menu ID (OR). | query | array | false | ?menu_ids[]=8f6c...&menu_ids[]=a12b... | |
| order_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| order_source | 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. LEAFLINK EXTERNAL_BUYER INTERNAL_USER API |
query | array | false | ?order_source[]=LEAFLINK&order_source[]=API | |
| owner_ids | Filter to orders owned by any of these users, by user ID (OR). | query | array | false | ?owner_ids[]=8f6c...&owner_ids[]=a12b... | |
| payment_status | 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. NOT_PAID PARTIALLY_PAID FULLY_PAID OVER_PAID |
query | array | false | ?payment_status[]=FULLY_PAID&payment_status[]=PARTIALLY_PAID | |
| product_ids | 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. |
query | array | false | ?product_ids[]=b3e1c0d2-1a2b-4c3d-8e9f-0a1b2c3d4e5f | |
| search | 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. | query | string | false | SO-1024 | |
| shipped_from_license_ids | Filter to orders shipped from any of these licenses, by license ID (OR). | query | array | false | ?shipped_from_license_ids[]=8f6c...&shipped_from_license_ids[]=a12b... | |
| status | 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. PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
query | array | false | ?status[]=COMPLETED&status[]=DELIVERED | |
| total | 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). |
query | string | false | 100,500 | |
| updated_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Sales Order History report | SalesOrderHistoryReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Sales Order Item History report
Success scenario
GET /public/v1/reports/sales-order-item-history?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTg1MTQ5YWItZjkwMS00NGNjLWIyZTItYWY4ZTQwZTk1ZTkxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTY0IiwidHlwIjoiYWNjZXNzIn0.6DIIoIHv6vy2ebWHKVxeqmFtPNZE5MbjMdsdJkDc2wA
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 73a6832a8c22d8ee63aa475f21960645-bf7f0e19a12bd154-0
{
"data": [
{
"batch_number": null,
"brand": null,
"brand_id": null,
"category": "Some category 23",
"customer": "Company 165",
"customer_id": "00000000-0000-0000-0000-0000000000a6",
"default_unit_cost": null,
"default_unit_price": "1.00",
"default_wholesale_price": null,
"delivery_date": null,
"delivery_date_utc": null,
"due_date": "2026-08-24T09:01:03.348120",
"due_date_utc": "2026-08-24T16:01:03.348120Z",
"group": "Product Group 25",
"invoice_numbers": null,
"line_item_id": "4cb686fa-9c9c-4211-afc6-cc1f349e6ebf",
"order_date": "2026-07-01T05:00:00.000000",
"order_date_utc": "2026-07-01T12:00:00.000000Z",
"order_id": "d0c7607d-a17c-4f6e-b561-040dab472eeb",
"order_item_price": "10.00",
"order_number": "SO-2",
"product": "P1",
"product_id": "6052fb64-2fed-4601-a370-daed2eecf7f7",
"product_sku": "sku 58",
"quantity": "2",
"returned_quantity": "0",
"sales_rep": null,
"source_package": null,
"status": "PENDING",
"subcategory": "Some subcategory 25",
"upc": null,
"vendor": "Acme Vendor",
"vendor_id": "00000000-0000-0000-0000-00000000002d"
},
{
"batch_number": null,
"brand": null,
"brand_id": null,
"category": "Some category 26",
"customer": "Company 165",
"customer_id": "00000000-0000-0000-0000-0000000000a6",
"default_unit_cost": null,
"default_unit_price": "1.00",
"default_wholesale_price": null,
"delivery_date": null,
"delivery_date_utc": null,
"due_date": "2026-08-24T09:01:03.348120",
"due_date_utc": "2026-08-24T16:01:03.348120Z",
"group": "Product Group 26",
"invoice_numbers": null,
"line_item_id": "d013c6ee-9af2-491b-b3a4-c546c788be82",
"order_date": "2026-07-01T05:00:00.000000",
"order_date_utc": "2026-07-01T12:00:00.000000Z",
"order_id": "d0c7607d-a17c-4f6e-b561-040dab472eeb",
"order_item_price": "10.00",
"order_number": "SO-2",
"product": "P2",
"product_id": "5cb97a07-fe3a-4151-ab0a-d8f6789286c6",
"product_sku": "sku 64",
"quantity": "1",
"returned_quantity": "0",
"sales_rep": null,
"source_package": null,
"status": "PENDING",
"subcategory": "Some subcategory 28",
"upc": null,
"vendor": "Acme Vendor",
"vendor_id": "00000000-0000-0000-0000-00000000002d"
},
{
"batch_number": null,
"brand": null,
"brand_id": null,
"category": "Some category 23",
"customer": "Company 161",
"customer_id": "00000000-0000-0000-0000-0000000000a2",
"default_unit_cost": null,
"default_unit_price": "1.00",
"default_wholesale_price": null,
"delivery_date": null,
"delivery_date_utc": null,
"due_date": "2026-08-24T09:01:03.322477",
"due_date_utc": "2026-08-24T16:01:03.322477Z",
"group": "Product Group 25",
"invoice_numbers": null,
"line_item_id": "6155450a-38c4-4af9-affe-25c8236f7b67",
"order_date": "2026-07-01T05:00:00.000000",
"order_date_utc": "2026-07-01T12:00:00.000000Z",
"order_id": "96cd6f1c-0976-4480-a793-0f4285152236",
"order_item_price": "10.00",
"order_number": "SO-1",
"product": "P1",
"product_id": "6052fb64-2fed-4601-a370-daed2eecf7f7",
"product_sku": "sku 58",
"quantity": "3",
"returned_quantity": "0",
"sales_rep": null,
"source_package": null,
"status": "COMPLETED",
"subcategory": "Some subcategory 25",
"upc": null,
"vendor": "Acme Vendor",
"vendor_id": "00000000-0000-0000-0000-00000000002d"
},
{
"batch_number": null,
"brand": null,
"brand_id": null,
"category": "Some category 26",
"customer": "Company 161",
"customer_id": "00000000-0000-0000-0000-0000000000a2",
"default_unit_cost": null,
"default_unit_price": "1.00",
"default_wholesale_price": null,
"delivery_date": null,
"delivery_date_utc": null,
"due_date": "2026-08-24T09:01:03.322477",
"due_date_utc": "2026-08-24T16:01:03.322477Z",
"group": "Product Group 26",
"invoice_numbers": null,
"line_item_id": "d19a8179-26bd-4fd6-92a2-f3c1c19c0028",
"order_date": "2026-07-01T05:00:00.000000",
"order_date_utc": "2026-07-01T12:00:00.000000Z",
"order_id": "96cd6f1c-0976-4480-a793-0f4285152236",
"order_item_price": "10.00",
"order_number": "SO-1",
"product": "P2",
"product_id": "5cb97a07-fe3a-4151-ab0a-d8f6789286c6",
"product_sku": "sku 64",
"quantity": "5",
"returned_quantity": "0",
"sales_rep": null,
"source_package": null,
"status": "COMPLETED",
"subcategory": "Some subcategory 28",
"upc": null,
"vendor": "Acme Vendor",
"vendor_id": "00000000-0000-0000-0000-00000000002d"
}
],
"meta": {
"columns": [
{
"key": "line_item_id",
"label": "Line Item Id"
},
{
"key": "order_id",
"label": "Order Id"
},
{
"key": "order_date",
"label": "Order Date"
},
{
"key": "order_date_utc",
"label": "Order Date (UTC)"
},
{
"key": "delivery_date",
"label": "Delivery Date"
},
{
"key": "delivery_date_utc",
"label": "Delivery Date (UTC)"
},
{
"key": "due_date",
"label": "Due Date"
},
{
"key": "due_date_utc",
"label": "Due Date (UTC)"
},
{
"key": "order_number",
"label": "Order Number"
},
{
"key": "status",
"label": "Status"
},
{
"key": "product",
"label": "Product"
},
{
"key": "product_id",
"label": "Product Id"
},
{
"key": "product_sku",
"label": "Product SKU"
},
{
"key": "default_unit_cost",
"label": "Default Unit Cost"
},
{
"key": "default_unit_price",
"label": "Default Unit Price"
},
{
"key": "default_wholesale_price",
"label": "Default Wholesale Price"
},
{
"key": "brand",
"label": "Brand"
},
{
"key": "brand_id",
"label": "Brand Id"
},
{
"key": "vendor",
"label": "Vendor"
},
{
"key": "vendor_id",
"label": "Vendor Id"
},
{
"key": "order_item_price",
"label": "Order Item Price"
},
{
"key": "returned_quantity",
"label": "Returned Quantity"
},
{
"key": "quantity",
"label": "Quantity"
},
{
"key": "category",
"label": "Category"
},
{
"key": "subcategory",
"label": "Subcategory"
},
{
"key": "group",
"label": "Group"
},
{
"key": "customer",
"label": "Customer"
},
{
"key": "customer_id",
"label": "Customer Id"
},
{
"key": "sales_rep",
"label": "Sales Rep"
},
{
"key": "invoice_numbers",
"label": "Invoice Numbers"
},
{
"key": "upc",
"label": "UPC"
},
{
"key": "batch_number",
"label": "Batch Number"
},
{
"key": "source_package",
"label": "Source Package"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "sales_order_item_history"
}
}
Error scenario: invalid order_datetime range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a0048cfedd32597bc0421d094d766148-d566acbed5e709b6-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
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.
Scoping and date default:
• Rows are always scoped to the API key's company and to the orders the key's user is permitted to see.
• 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.
• 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.
Column set varies by company: • 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. • The trade-sample-package column is Metrc-only. • Any Order custom fields configured for the company are appended as extra columns keyed by the field's label.
Value formatting (rows mirror the CSV export):
• 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).
• 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.
• 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.
• An empty cell is returned as an empty string or null depending on the column; treat any non-core column as possibly blank.
Ordering and result size:
• 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.
Required permission: reports_permissions_sales_order_item_history — the same permission that gates the Sales Order Item History report in the app.
Request
GET /public/v1/reports/sales-order-item-history
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_ids | Keep only line items sourced from one of the given batches, by batch ID. Repeat the key for several (OR'd). | query | array | false | ?batch_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| brand_ids | Keep only line items whose product belongs to one of the given brands, by brand ID. Repeat the key for several (OR'd). | query | array | false | ?brand_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| company_relationship_group_ids | Keep only line items whose customer belongs to one of the given customer groups, by group ID. Repeat the key for several (OR'd). | query | array | false | ?company_relationship_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| company_relationship_ids | 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. | query | array | false | ?company_relationship_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| created_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| creator_ids | Keep only line items whose order was created by one of the given users, by user ID. Repeat the key for several (OR'd). | query | array | false | ?creator_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| delivery_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| due_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| matched_with_compliance_transfer | 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. |
query | boolean | false | true | |
| menu_ids | Keep only line items whose order was placed against one of the given menus, by menu ID. Repeat the key for several (OR'd). | query | array | false | ?menu_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| order_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| order_source | 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. LEAFLINK EXTERNAL_BUYER INTERNAL_USER API |
query | array | false | ?order_source[]=API&order_source[]=INTERNAL_USER | |
| owner_ids | 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). | query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| payment_status | 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. NOT_PAID PARTIALLY_PAID FULLY_PAID OVER_PAID |
query | array | false | ?payment_status[]=FULLY_PAID&payment_status[]=PARTIALLY_PAID | |
| product_group_ids | 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). | query | array | false | ?product_group_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| product_ids | Keep only line items for one of the given products, by product ID. Repeat the key for several (OR'd). | query | array | false | ?product_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| sample | 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. ONLY EXCLUDE |
query | string | false | ONLY | |
| search | 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. | query | string | false | SO-1001 | |
| shipped_from_license_ids | Keep only line items whose order ships from one of the given licenses, by license ID. Repeat the key for several (OR'd). | query | array | false | ?shipped_from_license_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| status | 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. PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
query | array | false | ?status[]=COMPLETED&status[]=DELIVERED | |
| total | 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. |
query | string | false | 100,500 | |
| trade_sample_packages | 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. ONLY EXCLUDE |
query | string | false | EXCLUDE | |
| updated_datetime | 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. |
query | string | false | 2026-01-01T00:00:00Z,2026-02-01T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Sales Order Item History report | SalesOrderItemHistoryReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Get the Sales Order Tax report
Success scenario
GET /public/v1/reports/sales-order-tax?order_datetime=2026-06-01T00%3A00%3A00Z%2C2026-08-01T00%3A00%3A00Z
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYjAyODkxNjUtZjZlNC00NzM2LTkzNzctY2NkNWQwNjQ3NzZiIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODA0IiwidHlwIjoiYWNjZXNzIn0.S5iMVCR1Yj_j74hg5WZtlGwWuuPoWy2Yi9QPew45dyI
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 3262402910dd27b88cefbbd1839574bc-9b3c4b6febda59f1-0
{
"data": [
{
"tax_rate": "5.00",
"tax_type": "City Tax",
"total_tax": "20.00"
},
{
"tax_rate": "27.00",
"tax_type": "Excise Tax",
"total_tax": "1099.00"
}
],
"meta": {
"columns": [
{
"key": "tax_type",
"label": "Tax Type"
},
{
"key": "tax_rate",
"label": "Tax Rate"
},
{
"key": "total_tax",
"label": "Total Tax"
}
],
"date_range": "May 31, 2026 to Jul 31, 2026",
"report": "sales_order_tax"
}
}
Error scenario: invalid order_datetime range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 63510e5255807ab5a40f0a37cf9d412e-918fa932f82aedf5-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"order_datetime"
],
"section": "query"
}
]
}
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.
Which 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.
• 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.
• 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.
Numeric 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.
Required permission: reports_permissions_sales_order_tax. Company admins are always authorized regardless of this permission.
Request
GET /public/v1/reports/sales-order-tax
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_ids | 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. | query | array | false | ?batch_ids[]=a1b2c3d4-...&batch_ids[]=e5f6... | |
| brand_ids | 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. | query | array | false | ?brand_ids[]=a1b2c3d4-...&brand_ids[]=e5f6... | |
| company_relationship_group_ids | 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. | query | array | false | ?company_relationship_group_ids[]=a1b2c3d4-...&company_relationship_group_ids[]=e5f6... | |
| company_relationship_ids | 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. | query | array | false | ?company_relationship_ids[]=a1b2c3d4-...&company_relationship_ids[]=e5f6... | |
| created_datetime | 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. |
query | string | false | ?created_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| creator_ids | 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. | query | array | false | ?creator_ids[]=a1b2c3d4-...&creator_ids[]=e5f6... | |
| delivery_datetime | 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. |
query | string | false | ?delivery_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| due_datetime | 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. |
query | string | false | ?due_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| matched_with_compliance_transfer | 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. |
query | boolean | false | ?matched_with_compliance_transfer=true | |
| menu_ids | 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. | query | array | false | ?menu_ids[]=a1b2c3d4-...&menu_ids[]=e5f6... | |
| order_datetime | 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. |
query | string | false | ?order_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z | |
| order_source | 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. LEAFLINK EXTERNAL_BUYER INTERNAL_USER API |
query | array | false | ?order_source[]=INTERNAL_USER&order_source[]=API | |
| owner_ids | 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. | query | array | false | ?owner_ids[]=a1b2c3d4-...&owner_ids[]=e5f6... | |
| payment_status | 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. NOT_PAID PARTIALLY_PAID FULLY_PAID OVER_PAID |
query | array | false | ?payment_status[]=FULLY_PAID&payment_status[]=PARTIALLY_PAID | |
| product_ids | 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. | query | array | false | ?product_ids[]=a1b2c3d4-...&product_ids[]=e5f6... | |
| search | 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. | query | string | false | ||
| shipped_from_license_ids | 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. | query | array | false | ?shipped_from_license_ids[]=a1b2c3d4-...&shipped_from_license_ids[]=e5f6... | |
| status | 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. PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
query | array | false | ?status[]=COMPLETED&status[]=DELIVERED | |
| tax_ids | 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. |
query | array | false | ?tax_ids[]=a1b2c3d4-...&tax_ids[]=e5f6... | |
| total | 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. |
query | string | false | ?total=100,500 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The Sales Order Tax report | SalesOrderTaxReport |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Return
Get a return
Success scenario
GET /public/v1/returns/00000000-0000-0000-0000-000000000010
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZGYyZDJlN2YtMTkzMC00NDZkLTliODktNGYyOWIzNzEwMzU1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NTY2IiwidHlwIjoiYWNjZXNzIn0.5EUFfHYxn4wHUWOgQIt2uQlbqjHSPOfYfXdrG5_MmsM
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 841c2cbb74ad76903359964568a29e80-f5f66bed157a43cb-0
{
"data": {
"company": {
"id": "00000000-0000-0000-0000-00000000009f",
"name": "Company 481",
"updated_datetime": "2026-08-24T16:01:04.885130Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-569@example.com",
"full_name": "FirstName1162 LastName1163",
"id": "00000000-0000-0000-0000-00000000023b",
"inserted_datetime": "2026-08-24T16:01:04.814658Z",
"role": {
"id": "00000000-0000-0000-0000-000000000249",
"name": "Admin 584"
}
},
"credits": [
{
"amount": "100",
"credit_number": "CRT-RET",
"id": "4efabcd1-256f-4b87-9ea2-aa0567f8bba2",
"source": "RETURN"
}
],
"custom_data": {},
"description": null,
"id": "00000000-0000-0000-0000-000000000010",
"inserted_datetime": "2026-08-24T16:01:04.962071Z",
"invoice_numbers": [
"INV-001",
"INV-002"
],
"items": [
{
"id": "00000000-0000-0000-0000-000000000010",
"order_item": {
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-00000000003c",
"name": "B208"
},
"compliance_quantity": null,
"id": "108ba232-c5a4-4a6d-b622-8550140cdcc5",
"is_sample": false,
"location": null,
"package": null,
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "0f286c49-40fa-4ebc-b5bd-d7a42e76b7dd",
"name": "Product 203",
"sku": "sku 204",
"updated_datetime": "2026-08-24T16:01:04.871958Z"
},
"quantity": "5.000000000"
},
"quantity": 5.0,
"waste": false
}
],
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-0000000001cd",
"id": "00000000-0000-0000-0000-000000000069",
"license_id": null,
"name": "Place 104"
},
"order": {
"id": "4d404dfb-b8ef-4860-8493-1eff0b57397a",
"order_number": "SO-100",
"status": "PROCESSING",
"total": "0.00"
},
"order_quantity": "5",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-569@example.com",
"full_name": "FirstName1162 LastName1163",
"id": "00000000-0000-0000-0000-00000000023b",
"inserted_datetime": "2026-08-24T16:01:04.814658Z",
"role": {
"id": "00000000-0000-0000-0000-000000000249",
"name": "Admin 584"
}
},
"qb_credit_memo_id": "QB-CM-1",
"return_datetime": "2026-08-24T16:01:04.961708Z",
"return_number": "RN-15",
"return_quantity": "5",
"return_type": "Full Return",
"status": "PROCESSING",
"total": 32.0,
"updated_datetime": "2026-08-24T16:01:04.962071Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 0ca77822fafe62a3781d00527eb9e39e-9ed27f0ed4c7f375-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Returns 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.
Required permission: returns_permissions_view.
Request
GET /public/v1/returns/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | Distru ID of the return — the same id string returned as id in a return response. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single return | ReturnResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get returns
Success scenario
GET /public/v1/returns
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiMzllZWY0YzktNDdhZS00MWVkLWI2MzEtMTg0NTdkMTQyMDU3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTI5MiIsInR5cCI6ImFjY2VzcyJ9.uJUJ51426dzZbgSAJjiZM6ll647ojsO4OEkWIMkg46Q
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 36a1096416e2ae1724c01436cca1b2c1-7e795a3fed7e39df-0
{
"data": [
{
"company": {
"id": "00000000-0000-0000-0000-000000000171",
"name": "Company 1011",
"updated_datetime": "2026-08-24T16:01:07.473377Z"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-1291@example.com",
"full_name": "FirstName2622 LastName2623",
"id": "00000000-0000-0000-0000-00000000050e",
"inserted_datetime": "2026-08-24T16:01:07.420001Z",
"role": {
"id": "00000000-0000-0000-0000-000000000530",
"name": "Admin 1327"
}
},
"credits": [
{
"amount": "100",
"credit_number": "CRT-RET",
"id": "791c570b-5551-42ad-b3c3-704d06d39ece",
"source": "RETURN"
}
],
"custom_data": {},
"description": null,
"id": "00000000-0000-0000-0000-00000000001f",
"inserted_datetime": "2026-08-24T16:01:07.558964Z",
"invoice_numbers": [
"INV-001",
"INV-002"
],
"items": [
{
"id": "00000000-0000-0000-0000-00000000001e",
"order_item": {
"batch": {
"batch_number": null,
"id": "00000000-0000-0000-0000-000000000083",
"name": "B475"
},
"compliance_quantity": null,
"id": "435f7399-a4a0-4d75-ac27-2375b76cee10",
"is_sample": false,
"location": null,
"package": null,
"price": "10.000000000",
"price_base": "10",
"product": {
"id": "ec6f6efe-6a9c-4643-8580-3f736f00f699",
"name": "Product 470",
"sku": "sku 471",
"updated_datetime": "2026-08-24T16:01:07.457126Z"
},
"quantity": "10.000000000"
},
"quantity": 10.0,
"waste": false
}
],
"location": {
"address": "123 Fake Street, Beverly Hills, CA 90210, US",
"company_id": "00000000-0000-0000-0000-0000000003e5",
"id": "00000000-0000-0000-0000-000000000103",
"license_id": null,
"name": "Place 258"
},
"order": {
"id": "4a96aacc-4e55-45ad-9838-8529d6256456",
"order_number": "SO-100",
"status": "PROCESSING",
"total": "0.00"
},
"order_quantity": "10",
"owner": {
"banned": false,
"deleted_at": null,
"email": "owner-1291@example.com",
"full_name": "FirstName2622 LastName2623",
"id": "00000000-0000-0000-0000-00000000050e",
"inserted_datetime": "2026-08-24T16:01:07.420001Z",
"role": {
"id": "00000000-0000-0000-0000-000000000530",
"name": "Admin 1327"
}
},
"qb_credit_memo_id": null,
"return_datetime": "2026-08-24T16:01:07.558483Z",
"return_number": "RN-30",
"return_quantity": "10",
"return_type": "Full Return",
"status": "PROCESSING",
"total": 32.0,
"updated_datetime": "2026-08-24T16:01:07.558964Z"
}
],
"next_page": null
}
Error scenario: invalid page number
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c323eeacd669dd8cb4fd74e7f75c3a65-80e6329f5273db2c-0
{
"errors": [
{
"context": {},
"message": "must be greater than 0",
"pointer": [
"page",
"number"
],
"section": "query"
}
]
}
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.
A 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, which can sync to QuickBooks Online as a credit memo. A return is usually tied to the original sales order; 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.
Returned 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.
Results 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.
Required permission: returns_permissions_view.
Request
GET /public/v1/returns
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| company_ids | 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. |
query | array | false | ?company_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| custom_data | 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. |
query | object | false | ?custom_data[101]=Blue&custom_data[102]=Wholesale | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| order_ids | 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. |
query | array | false | ?order_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| owner_ids | 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. |
query | array | false | ?owner_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| return_datetime | 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. |
query | string | false | ?return_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| statuses | 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. PROCESSING SHIPPED RECEIVED COMPLETED |
query | array | false | ?statuses[]=PROCESSING&statuses[]=SHIPPED | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of returns | Returns |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
StockAdjustment
Get a stock adjustment
Success scenario
GET /public/v1/adjustments/00000000-0000-0000-0000-00000000003e
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzAsImlhdCI6MTc4NzU4NzI3MCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNDhiNzdmN2ItNWRjOS00MDE3LWFjYzQtMGQ0NTBiZjNkNjlkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY5LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjE4MyIsInR5cCI6ImFjY2VzcyJ9.HoNUd1FrgCZn6lnmkiulPkZQ-5rh6iBqWKKHaSqpAUs
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: c1a1bafcd36d989448b1a8306c0b7a34-172755c6308db0f9-0
{
"data": {
"batch_id": "00000000-0000-0000-0000-000000000149",
"completion_datetime": "2026-08-24T16:01:10.137959Z",
"compliance_quantity": null,
"compliance_unit_type": null,
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-2190@example.com",
"full_name": "FirstName4464 LastName4465",
"id": "00000000-0000-0000-0000-000000000897",
"inserted_datetime": "2026-08-24T16:01:10.133960Z",
"role": {
"id": "00000000-0000-0000-0000-0000000008c3",
"name": "Admin 2242"
}
},
"description": null,
"id": "00000000-0000-0000-0000-00000000003e",
"inserted_datetime": "2026-08-24T16:01:10.142345Z",
"license_id": null,
"location_id": "00000000-0000-0000-0000-0000000001ab",
"owner_id": null,
"package_id": null,
"product_id": "ec2bfff7-3949-48b2-9838-d39edafa26b0",
"quantity": "10",
"reason": "revaluation",
"total_cost": null,
"unit_cost": null,
"unit_type": {
"id": "00000000-0000-0000-0000-000000004b93",
"name": "Gram"
},
"updated_datetime": "2026-08-24T16:01:10.142345Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 7f2251b3be4746c57a9509c459028d03-16d2dba676daf211-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Required permission: products_permissions_view.
Request
GET /public/v1/adjustments/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the stock adjustment to fetch. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single stock adjustment | StockAdjustmentResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get adjustments
Success scenario
GET /public/v1/adjustments
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzEsImlhdCI6MTc4NzU4NzI3MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNGNkMmZlNDItYjRmNC00ODgzLWE0YzItZjI5M2QyYTVhYTc5IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjQ2MiIsInR5cCI6ImFjY2VzcyJ9.SWTdIU9TILr10Z_allxBKFR4H_gJLT9_hCUKjq4ilkA
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 32882a56c639ba629baa14c812e454eb-3967888447898923-0
{
"data": [
{
"batch_id": null,
"completion_datetime": "2026-08-24T16:01:11.052888Z",
"compliance_quantity": null,
"compliance_unit_type": null,
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-2464@example.com",
"full_name": "FirstName5024 LastName5025",
"id": "00000000-0000-0000-0000-0000000009ab",
"inserted_datetime": "2026-08-24T16:01:11.048623Z",
"role": {
"id": "00000000-0000-0000-0000-0000000009db",
"name": "Admin 2522"
}
},
"description": null,
"id": "00000000-0000-0000-0000-000000000062",
"inserted_datetime": "2026-08-24T16:01:11.055614Z",
"license_id": null,
"location_id": null,
"owner_id": "00000000-0000-0000-0000-00000000099e",
"package_id": null,
"product_id": "dd1b3b00-48c1-47cd-84b3-4656f26433a0",
"quantity": "10",
"reason": "revaluation",
"total_cost": "10000",
"unit_cost": "1000",
"unit_type": {
"id": "00000000-0000-0000-0000-0000000054f5",
"name": "Gram"
},
"updated_datetime": "2026-08-24T16:01:11.055614Z"
},
{
"batch_id": null,
"completion_datetime": "2026-08-24T16:01:11.263282Z",
"compliance_quantity": "1",
"compliance_unit_type": {
"id": "00000000-0000-0000-0000-0000000054f7",
"name": "Ounce"
},
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-2451@example.com",
"full_name": "FirstName4998 LastName4999",
"id": "00000000-0000-0000-0000-00000000099e",
"inserted_datetime": "2026-08-24T16:01:10.983389Z",
"role": {
"id": "00000000-0000-0000-0000-0000000009cd",
"name": "Admin 2508"
}
},
"description": "A default note describing this transaction",
"id": "00000000-0000-0000-0000-000000000064",
"inserted_datetime": "2026-08-24T16:01:11.273231Z",
"license_id": "00000000-0000-0000-0000-00000000005f",
"location_id": "00000000-0000-0000-0000-0000000001db",
"owner_id": null,
"package_id": "00000000-0000-0000-0000-000000000044",
"product_id": "06f22c33-e630-4584-a324-dfa1d397a2eb",
"quantity": "1",
"reason": "Voluntary Surrender",
"total_cost": "900",
"unit_cost": "900",
"unit_type": {
"id": "00000000-0000-0000-0000-0000000054f7",
"name": "Ounce"
},
"updated_datetime": "2026-08-24T16:01:11.273231Z"
},
{
"batch_id": "00000000-0000-0000-0000-000000000194",
"completion_datetime": "2026-08-24T16:01:11.410696Z",
"compliance_quantity": null,
"compliance_unit_type": null,
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-2555@example.com",
"full_name": "FirstName5206 LastName5207",
"id": "00000000-0000-0000-0000-000000000a07",
"inserted_datetime": "2026-08-24T16:01:11.405684Z",
"role": {
"id": "00000000-0000-0000-0000-000000000a3c",
"name": "Admin 2619"
}
},
"description": null,
"id": "00000000-0000-0000-0000-000000000067",
"inserted_datetime": "2026-08-24T16:01:11.414641Z",
"license_id": null,
"location_id": "00000000-0000-0000-0000-0000000001d8",
"owner_id": null,
"package_id": null,
"product_id": "f691ec6b-3a30-45d7-9b65-f946d0e45eeb",
"quantity": "1",
"reason": "revaluation",
"total_cost": "-800",
"unit_cost": "-800",
"unit_type": {
"id": "00000000-0000-0000-0000-0000000054f5",
"name": "Gram"
},
"updated_datetime": "2026-08-24T16:01:11.414641Z"
}
],
"next_page": null
}
Error scenario: invalid inserted_datetime
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b472d2df140359ae52c577d7cde9d4e1-570c73d38c8c1334-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"inserted_datetime"
],
"section": "query"
}
]
}
List stock adjustments for your company, oldest first (by creation time), with optional filters.
A 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.
Narrow 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).
Results 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.
This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.
Required permission: products_permissions_view.
Request
GET /public/v1/adjustments
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_ids | 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. |
query | array | false | ?batch_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| completion_datetime | 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. |
query | string | false | ?completion_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| location_ids | 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. |
query | array | false | ?location_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| product_ids | 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. |
query | array | false | ?product_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of stock adjustments | StockAdjustments |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Insert a stock adjustment
Success scenario
POST /public/v1/adjustments
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzIsImlhdCI6MTc4NzU4NzI3MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmViNGY1MDMtNWJlZC00MTM0LTliYzQtMzlkZTUxNTY5OTQ3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjcxNiIsInR5cCI6ImFjY2VzcyJ9.Ail7xS0z-JY6D4Z0_a25q4lQ0GN8xU1xj55tnVPEpa0
{
"completion_datetime": "2020-01-03T12:20:00.000000Z",
"description": "test",
"location_id": "00000000-0000-0000-0000-00000000020a",
"product_id": "60c19989-3549-4a81-b117-7bf61307fb24",
"quantity": 10,
"reason": "expired"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4c9ccdc5e119b0569676cba519c06b08-5968331e2ca84837-0
{
"data": {
"batch_id": null,
"completion_datetime": "2020-01-03T12:20:00.000000Z",
"compliance_quantity": null,
"compliance_unit_type": null,
"creator": {
"banned": false,
"deleted_at": null,
"email": "owner-2702@example.com",
"full_name": "FirstName5502 LastName5503",
"id": "00000000-0000-0000-0000-000000000a9c",
"inserted_datetime": "2026-08-24T16:01:12.013633Z",
"role": {
"id": "00000000-0000-0000-0000-000000000ad6",
"name": "Admin 2773"
}
},
"description": "test",
"id": "00000000-0000-0000-0000-000000000070",
"inserted_datetime": "2026-08-24T16:01:12.086602Z",
"license_id": null,
"location_id": "00000000-0000-0000-0000-00000000020a",
"owner_id": null,
"package_id": null,
"product_id": "60c19989-3549-4a81-b117-7bf61307fb24",
"quantity": "10",
"reason": "expired",
"total_cost": null,
"unit_cost": null,
"unit_type": {
"id": "00000000-0000-0000-0000-000000005def",
"name": "Gram"
},
"updated_datetime": "2026-08-24T16:01:12.086602Z"
}
}
Error scenario: product not found
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f68a9a3f38b83bffe866e1478c27e391-592425b70fa8e266-0
{
"errors": [
{
"context": {},
"message": "Product not found",
"pointer": [
"product_id"
],
"section": "body"
}
]
}
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).
Identify 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.
There are two adjustment modes, selected by whether you pass package_id:
• 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.
• 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.
The 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.
Stock adjustments are immutable: once created they cannot be edited or deleted through this API.
Required 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.
Request
POST /public/v1/adjustments
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_id | 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. |
body | string | false | ||
| completion_datetime | 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. |
body | string | false | ||
| compliance_quantity | 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. |
body | number | false | ||
| 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. |
body | string | false | ||
| location_id | 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. |
body | string | false | ||
| package_id | 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. |
body | string | false | ||
| product_id | 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. |
body | string | false | ||
| quantity | 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. |
body | number | false | ||
| reason | 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. |
body | string | false | ||
| unit_cost | 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. |
body | number | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The stock adjustment was inserted successfully | StockAdjustmentResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Strain
Create or update a strain
Success scenario
POST /public/v1/strains
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMjNjNzJlYzAtZTAyMi00MzE2LTg5NzgtNGE5NDZhMjIwMzM1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODgwIiwidHlwIjoiYWNjZXNzIn0.cFjw3EBH-ZHrnMXUVFw3vO14UIYrijUUDYqklVcFbec
{
"name": "Blue Dream",
"strain_type": "HYBRID"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: b3ac47b52e0a96385152be5f79346faa-2ed0b5764016b64d-0
{
"data": {
"id": "00000000-0000-0000-0000-000000000028",
"inserted_datetime": "2026-08-24T16:01:06.029006Z",
"name": "Blue Dream",
"strain_type": "HYBRID",
"updated_datetime": "2026-08-24T16:01:06.029006Z"
}
}
Error scenario: missing name
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4dd2d20547945bdb04c1d13ec6c7c4ec-d1b3485eb40dd4a1-0
{
"errors": [
{
"context": {},
"message": "can't be blank",
"pointer": [
"name"
],
"section": "body"
}
]
}
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.
Updates 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.
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.
Required permission: settings_permissions_strains.
Request
POST /public/v1/strains
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | body | string | false | ||
| name | 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. |
body | string | false | ||
| strain_type | 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.INDICA INDICA_DOMINANT SATIVA SATIVA_DOMINANT HYBRID HIGH_CBD |
body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The created or updated strain | StrainResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Delete a strain
Success scenario
DELETE /public/v1/strains/00000000-0000-0000-0000-00000000001e
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMjNjNzJlYzAtZTAyMi00MzE2LTg5NzgtNGE5NDZhMjIwMzM1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODgwIiwidHlwIjoiYWNjZXNzIn0.cFjw3EBH-ZHrnMXUVFw3vO14UIYrijUUDYqklVcFbec
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 5448ba9743f5da802811d6c7d10286db-5a5edcd33594b7bb-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ba814631d8868f824545ba38e52cf1fa-ff618aa77fb9cfae-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Deleting 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.
A 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.
Required permission: settings_permissions_strains.
Request
DELETE /public/v1/strains/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a strain
Success scenario
GET /public/v1/strains/00000000-0000-0000-0000-00000000001d
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiODExYTRmYjItYWJiYy00ZDAyLTk2N2UtNzczNGVkMTRhYzdjIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NjUwIiwidHlwIjoiYWNjZXNzIn0.pRl3nSLxxNKdgJ_QJi-Y7RdW9YvH7JnWBlkd93GowZE
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: d8aaab1d10367de8489fb14c464deb08-99080e1a8572cabb-0
{
"data": {
"id": "00000000-0000-0000-0000-00000000001d",
"inserted_datetime": "2026-08-24T16:01:05.154912Z",
"name": "Blue Dream",
"strain_type": "HYBRID",
"updated_datetime": "2026-08-24T16:01:05.154912Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: fe37e0687be9f0d99cf9e2233b928e68-a904e23604446ed3-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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).
Required permission: settings_permissions_strains.
Request
GET /public/v1/strains/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single strain | StrainResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get strains
Success scenario
GET /public/v1/strains
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMjRhZWM4NDQtNjc5MS00MDI2LWJjYmEtOTcwMDQ1MWIzYmE5IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODgiLCJ0eXAiOiJhY2Nlc3MifQ.SsEkWqZ3h399dVeBdao9BclrtY7BEBgZdJhMnjBLh7A
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 50f669287644067652c88937ee75eebc-f4d782b553491e22-0
{
"data": [
{
"id": "00000000-0000-0000-0000-000000000004",
"inserted_datetime": "2026-08-24T16:01:02.571947Z",
"name": "Strain 3",
"strain_type": "INDICA",
"updated_datetime": "2026-08-24T16:01:02.571947Z"
},
{
"id": "00000000-0000-0000-0000-000000000005",
"inserted_datetime": "2026-08-24T16:01:02.574215Z",
"name": "Strain 4",
"strain_type": null,
"updated_datetime": "2026-08-24T16:01:02.574215Z"
}
],
"next_page": null
}
Error scenario: invalid page number
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4a9f9d9eac16aba95cc06cf500f44c06-cd9581f38707d5b5-0
{
"errors": [
{
"context": {},
"message": "must be greater than 0",
"pointer": [
"page",
"number"
],
"section": "query"
}
]
}
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.
Results 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.
Responses are eventually consistent: a strain you just created or updated may take up to 1 second to appear here or reflect its latest values.
Required permission: settings_permissions_strains.
Request
GET /public/v1/strains
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z, | |
| name | Case-insensitive substring match on the strain name (partial matches count; e.g. kush matches OG Kush). Send a single value, not a list. |
query | string | false | ?name=kush | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| types | 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.INDICA INDICA_DOMINANT SATIVA SATIVA_DOMINANT HYBRID HIGH_CBD |
query | array | false | ?types[]=INDICA&types[]=SATIVA | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of strains | Strains |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Tag
Delete a tag
Success scenario
DELETE /public/v1/tags/00000000-0000-0000-0000-00000000001a
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjgsImlhdCI6MTc4NzU4NzI2OCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMWQ5ZTE5NTktZTQ1Yy00ZGU2LTg0ZTctZjMyYTg1MGUyNGZlIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY3LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTgwMyIsInR5cCI6ImFjY2VzcyJ9.VXXiwgqrzoowiX2FbfXSgPuHhsgo4jqADUF99_WiIJw
Response
204
cache-control: max-age=0, private, must-revalidate
b3: aef9f9c4f15b93a24fe14d1cefc38d94-164cb27b196030b7-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 754640c93ec1be093f85b9513652d5f8-f8f5878e469a6586-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Permanently deletes the tag. This is a hard delete — the tag is gone for good and cannot be recovered; to reuse the name later you must create a fresh tag.
Deleting a tag also detaches it from everything it was applied to: it is removed from every product and every tax that carried it. Those products and taxes are not otherwise changed — only the tag label disappears from them. No inventory and no compliance system (Metrc/BioTrack) is affected.
Returns 404 when no tag with that ID exists for the authenticated company.
Any authenticated API key for the company may manage tags; no additional settings permission is required.
Request
DELETE /public/v1/tags/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the tag to delete. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a tag
Success scenario
GET /public/v1/tags/00000000-0000-0000-0000-00000000000a
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiZTVlOWQ3MmEtYjUwNy00OTcxLTg1MDktN2FkMzVjM2VkNzdlIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTE4NCIsInR5cCI6ImFjY2VzcyJ9.3BphjuLfCf96ocMhyxCDx8eEzi9UYjNffpsCtSalKDU
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ee43f1c971ee7a959703cb8f1b84b96b-4c90a3eee8bf4395-0
{
"data": {
"id": "00000000-0000-0000-0000-00000000000a",
"inserted_datetime": "2026-08-24T16:01:07.045841Z",
"name": "Top Shelf",
"updated_datetime": "2026-08-24T16:01:07.045841Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 91ede3da0a6114eb3b2ddc736ef2a7b4-8a60de1cf7b43529-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Fetches a single tag by its ID. Returns 404 when no tag with that ID exists for the authenticated company — IDs from another company are never visible.
Any authenticated API key for the company may read tags; no additional settings permission is required.
Request
GET /public/v1/tags/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the tag to fetch, as returned by the list and upsert endpoints. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single tag | TagResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get tags
Success scenario
GET /public/v1/tags
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjYsImlhdCI6MTc4NzU4NzI2NiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMmJmMTI3M2UtYzk4ZC00MjQwLTgzYWYtZDk2YmUzYjgzY2YwIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY1LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTAwNCIsInR5cCI6ImFjY2VzcyJ9.-Mvg7jtu1PihleDl1djKIGzAK6R4tt6DuniXlYXtXHc
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 996ce980cf577bd28e42ef19c39bf947-34a2813ce4848645-0
{
"data": [
{
"id": "00000000-0000-0000-0000-000000000005",
"inserted_datetime": "2026-08-24T16:01:06.428337Z",
"name": "T1",
"updated_datetime": "2026-08-24T16:01:06.428337Z"
},
{
"id": "00000000-0000-0000-0000-000000000006",
"inserted_datetime": "2026-08-24T16:01:06.429506Z",
"name": "T2",
"updated_datetime": "2026-08-24T16:01:06.429506Z"
},
{
"id": "00000000-0000-0000-0000-000000000007",
"inserted_datetime": "2026-08-24T16:01:06.430473Z",
"name": "T3",
"updated_datetime": "2026-08-24T16:01:06.430473Z"
}
],
"next_page": "https://www.example.com/public/v1/tags?page[number]=2"
}
Error scenario: invalid page number
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 09278787ff085a59ba586d11e8e25e37-a3a00bfc54a75666-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page",
"number"
],
"section": "query"
}
]
}
Lists every tag belonging to the authenticated company. Tags are the reusable labels you attach to products and taxes to categorize, group, and filter them.
Results are ordered by creation time, oldest first, and paginated. Narrow the set with the
inserted_datetime and updated_datetime range filters; otherwise it returns the full set for the
company, one page at a time. Follow next_page in the response until it is null to walk the whole list.
Any authenticated API key for the company may read tags; no additional settings permission is required.
Request
GET /public/v1/tags
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of tags | Tags |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a tag
Success scenario
POST /public/v1/tags
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiZGViZGU5ZTAtYzg3YS00YzNhLWFlZTItMmFmMGQwYjhhYjYyIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTI0NCIsInR5cCI6ImFjY2VzcyJ9.gkhgm56LydWV7eCMVWcp2QB17LHXB_UpFkwhGtEV-hM
{
"name": "Top Shelf"
}
Response
201
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 7c914301572bc3baa9fba04e219eec37-c1e4a2a2d1b04297-0
{
"data": {
"id": "00000000-0000-0000-0000-00000000000d",
"inserted_datetime": "2026-08-24T16:01:07.283106Z",
"name": "Top Shelf",
"updated_datetime": "2026-08-24T16:01:07.283106Z"
}
}
Error scenario: missing name
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 2481e73bbf3832dfb01333aa68155c90-6fb9127b94e600e1-0
{
"errors": [
{
"context": {},
"message": "Please enter a tag",
"pointer": [
"name"
],
"section": "body"
}
]
}
Creates a new tag or renames an existing one. Pass id to update the tag with that ID; omit id to
create a new tag. A create returns 201, an update returns 200; both return the saved tag.
A tag is only a label. Creating or renaming one does not attach it to anything, and touches no inventory and no compliance system (Metrc/BioTrack) — these are Distru-internal labels, unrelated to Metrc's physical package tags. Renaming a tag keeps it attached to every product and tax it was already on, so those records simply reflect the new name.
name must be unique within the company, compared case-insensitively — "Indica" and "indica" collide, and
a duplicate is rejected with a validation error. Updating a tag to a name already used by another tag is
likewise rejected.
Any authenticated API key for the company may manage tags; no additional settings permission is required.
Request
POST /public/v1/tags
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | body | string | false | ||
| name | 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. |
body | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The updated tag | TagResponse |
| 201 | The created tag | TagResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Tax
Get a tax
Success scenario
GET /public/v1/taxes/00000000-0000-0000-0000-000000000009
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYzJhNzMzNzQtODNkNS00YjQyLTljYzItNWUyYjY2OGVmNDAxIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6Mjg1IiwidHlwIjoiYWNjZXNzIn0.Scq-PC5Fz9TMdgNUky6jmTeoIkvhwBhJsi-Z5uhsb0c
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: e7f330cbbac603f24d69f86b6d33a09f-1088e49345af1a7e-0
{
"data": {
"description": null,
"id": "00000000-0000-0000-0000-000000000009",
"inserted_datetime": "2026-08-24T16:01:03.788593Z",
"name": "CA Excise",
"qb_account_id": "84",
"qb_product_id": "12",
"tags": [
{
"id": "00000000-0000-0000-0000-000000000003",
"name": "Cannabis"
}
],
"tax_applied_after_charges": true,
"tax_applied_after_price_tiers": true,
"tax_code": "EXCISE",
"tax_rate_percent": 15.0,
"updated_datetime": "2026-08-24T16:01:03.789263Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 758853b167a464fafa25816a6fa436e6-ac66d68c1a5b085d-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Fetch a single tax by its ID. A tax is a named, reusable tax rate (percentage) applied to order and invoice line totals; use this to resolve a tax id seen on those records into its current rate, code, and QuickBooks Online mappings.
Returns 404 when no tax with that ID exists for the authenticated company, or when the tax has been deleted. This endpoint is read-only and touches no other data.
Required permission: settings_permissions_taxes.
Request
GET /public/v1/taxes/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The tax's ID, as returned in the id field of a tax response. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single tax | TaxResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get taxes
Success scenario
GET /public/v1/taxes
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZDAyOGU0ZDYtOTVhNy00ZjY5LWI2MjUtMWQxYTlmYjE3ZDQ1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6ODQiLCJ0eXAiOiJhY2Nlc3MifQ.y2heqq-IE1jRpYhFkmSVGKS32Se9915XPPuUVRXGswQ
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f24c3f2b42f3e561c19e8a56d8dfe442-fe05c772eb59f080-0
{
"data": [
{
"description": null,
"id": "00000000-0000-0000-0000-000000000001",
"inserted_datetime": "2026-08-24T16:01:02.569361Z",
"name": "T1",
"qb_account_id": null,
"qb_product_id": null,
"tags": [
{
"id": "00000000-0000-0000-0000-000000000001",
"name": "Cannabis"
}
],
"tax_applied_after_charges": false,
"tax_applied_after_price_tiers": true,
"tax_code": "Tax Code 1",
"tax_rate_percent": 15.0,
"updated_datetime": "2026-08-24T16:01:02.569361Z"
},
{
"description": null,
"id": "00000000-0000-0000-0000-000000000002",
"inserted_datetime": "2026-08-24T16:01:02.583160Z",
"name": "T2",
"qb_account_id": null,
"qb_product_id": null,
"tags": [],
"tax_applied_after_charges": false,
"tax_applied_after_price_tiers": true,
"tax_code": "Tax Code 3",
"tax_rate_percent": 15.0,
"updated_datetime": "2026-08-24T16:01:02.583160Z"
},
{
"description": null,
"id": "00000000-0000-0000-0000-000000000003",
"inserted_datetime": "2026-08-24T16:01:02.596040Z",
"name": "T3",
"qb_account_id": null,
"qb_product_id": null,
"tags": [],
"tax_applied_after_charges": false,
"tax_applied_after_price_tiers": true,
"tax_code": "Tax Code 5",
"tax_rate_percent": 15.0,
"updated_datetime": "2026-08-24T16:01:02.596040Z"
}
],
"next_page": "https://www.example.com/public/v1/taxes?page[number]=2"
}
Error scenario: invalid page param
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 61538e0824e3e2837b1c929e78b387b1-5a753dc5ffb0fadd-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
List the taxes configured for the authenticated company. A tax is a named, reusable tax rate (percentage) that gets applied to order and invoice line totals. Use this to enumerate the taxes you can reference elsewhere and to read their current rates, codes, and QuickBooks Online mappings.
Results are ordered oldest-first (by creation time) and paginated. Soft-deleted taxes are never returned. This endpoint is read-only — taxes cannot be created, edited, or deleted through the public API — so it has no effect on inventory, compliance, or any other entity.
Required permission: settings_permissions_taxes.
Request
GET /public/v1/taxes
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of taxes | Taxes |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
TestResult
Delete a test result
Success scenario
DELETE /public/v1/test-results/dbf34ffb-5e7e-4b7d-9b41-cf381f27b4bc
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzMsImlhdCI6MTc4NzU4NzI3MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmE0NTNlYjgtNjRkZS00NTNlLWI0NGEtOGE3ZWYxYTBjYzk1IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjcyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzE0MiIsInR5cCI6ImFjY2VzcyJ9.prQ9oJP7X1X_OLC9F0AlSUp3TwgJHRCOSQn0mm-JP5o
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 8c2f5a1d94e3ba702811d6c7d10286db-4b7edcd33594b7aa-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cf914631d8868f824545ba38e52cf1ab-aa618aa77fb9cfbe-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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).
Only 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.
Effects 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 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.
Required permission: products_permissions_edit.
Request
DELETE /public/v1/test-results/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a test result
Success scenario
GET /public/v1/test-results/00000000-0000-0000-0000-000000000014
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NzAsImlhdCI6MTc4NzU4NzI3MCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiZGU2YmVkNmEtODhmMC00ZmNiLWFlOGQtNmViMjRhMGEwZGNjIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY5LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjI4MyIsInR5cCI6ImFjY2VzcyJ9.SMfCNJkTRcu8TKaOIvlED51aS_vQCvsCf9X5FI9U2Lo
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 5801955beb3726887499a605e92be30c-1b083cfcb5a7429f-0
{
"data": {
"additional_test_results": {},
"batch_id": "00000000-0000-0000-0000-000000000155",
"biotrack_id": null,
"cbd_mg_per_unit": null,
"cbd_percentage": null,
"coa_url": null,
"id": "00000000-0000-0000-0000-000000000014",
"inserted_datetime": "2026-08-24T16:01:10.419016Z",
"is_primary": false,
"lab_license_number": null,
"lab_name": null,
"metrc_id": null,
"mg_per_unit_type": "mg/g",
"name": "TR001",
"package_id": null,
"release_date": null,
"thc_mg_per_unit": null,
"thc_percentage": null,
"total_cbd_mg_per_unit": null,
"total_cbd_percentage": null,
"total_thc_mg_per_unit": null,
"total_thc_percentage": null,
"updated_datetime": "2026-08-24T16:01:10.419016Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 045c81f7053d62feb9698f9f65030373-366bc84164181f86-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Required permission: products_permissions_view.
Request
GET /public/v1/test-results/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The ID of the test result to fetch, as returned by the list and upsert endpoints. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single test result | TestResultResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get test results
Success scenario
GET /public/v1/test-results
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjcsImlhdCI6MTc4NzU4NzI2NywiaXNzIjoiRGlzdHJ1IiwianRpIjoiZjhlNjhkODgtZjI0NS00MTMzLThhY2UtMDA3NzMxNzY4NTRlIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY2LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTE4MiIsInR5cCI6ImFjY2VzcyJ9.liF3bphWdf-Ps0xTjxBFx2XNVvNL24nAKH18toIG1do
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 97326a22b3d386c99d7a4a628be9bcf4-d7ef5bfcb239f30d-0
{
"data": [
{
"additional_test_results": {
"thca_percentage": "12"
},
"batch_id": null,
"biotrack_id": null,
"cbd_mg_per_unit": "1.12345",
"cbd_percentage": "60.1234",
"coa_url": null,
"id": "00000000-0000-0000-0000-000000000004",
"inserted_datetime": "2026-08-24T16:01:07.249059Z",
"is_primary": false,
"lab_license_number": "1234567890",
"lab_name": "Test Lab",
"metrc_id": 1234567890,
"mg_per_unit_type": "mg/g",
"name": "Test result 1",
"package_id": "00000000-0000-0000-0000-000000000018",
"release_date": "2026-08-24",
"thc_mg_per_unit": "2.12345",
"thc_percentage": "20.1234",
"total_cbd_mg_per_unit": "3.12345",
"total_cbd_percentage": "80.1234",
"total_thc_mg_per_unit": "4.12345",
"total_thc_percentage": "40.1234",
"updated_datetime": "2026-08-24T16:01:07.249059Z"
},
{
"additional_test_results": {
"thca_percentage": "12"
},
"batch_id": "00000000-0000-0000-0000-000000000078",
"biotrack_id": null,
"cbd_mg_per_unit": null,
"cbd_percentage": null,
"coa_url": null,
"id": "00000000-0000-0000-0000-000000000005",
"inserted_datetime": "2026-08-24T16:01:07.270964Z",
"is_primary": false,
"lab_license_number": null,
"lab_name": null,
"metrc_id": null,
"mg_per_unit_type": "mg/g",
"name": "File.pdf",
"package_id": null,
"release_date": null,
"thc_mg_per_unit": null,
"thc_percentage": null,
"total_cbd_mg_per_unit": null,
"total_cbd_percentage": null,
"total_thc_mg_per_unit": null,
"total_thc_percentage": null,
"updated_datetime": "2026-08-24T16:01:07.270964Z"
},
{
"additional_test_results": {},
"batch_id": null,
"biotrack_id": null,
"cbd_mg_per_unit": null,
"cbd_percentage": null,
"coa_url": null,
"id": "00000000-0000-0000-0000-000000000006",
"inserted_datetime": "2026-08-24T16:01:07.398462Z",
"is_primary": false,
"lab_license_number": null,
"lab_name": null,
"metrc_id": null,
"mg_per_unit_type": "mg/g",
"name": "File.pdf",
"package_id": "00000000-0000-0000-0000-00000000001b",
"release_date": null,
"thc_mg_per_unit": null,
"thc_percentage": null,
"total_cbd_mg_per_unit": null,
"total_cbd_percentage": null,
"total_thc_mg_per_unit": null,
"total_thc_percentage": null,
"updated_datetime": "2026-08-24T16:01:07.398462Z"
}
],
"next_page": null
}
Error scenario: invalid date range
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 5d7daa1d05bb599ee95028af403b7ddc-2a49236a5dab0e01-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"updated_datetime"
],
"section": "query"
}
]
}
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.
Results 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.
Required permission: products_permissions_view.
Request
GET /public/v1/test-results
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| batch_ids | 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. |
query | array | false | ?batch_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=,2022-07-10T00:00:00Z | |
| metrc_ids | 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. |
query | array | false | ?metrc_ids[]=84213&metrc_ids[]=84214 | |
| package_ids | 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. |
query | array | false | ?package_ids[]=550e8400-e29b-41d4-a716-446655440000 | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| product_ids | 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. | query | array | false | ?product_ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of test results | TestResults |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Upsert a test result
Success scenario
POST /public/v1/test-results
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjgsImlhdCI6MTc4NzU4NzI2OCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMTI5MTg4OTQtYzI2ZS00NjIzLTgxY2QtMDRlYjRhNDJkMWUyIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY3LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTczMSIsInR5cCI6ImFjY2VzcyJ9.9uEbeQdJ97tQXEEQEDiRQ3mnNXV72EjOYWAG4YHMEIk
{
"additional_test_results": {
"delta_8_thc_mg_per_unit": "17.3333",
"delta_8_thc_percentage": "12.55",
"thcva_percentage": "100"
},
"batch_id": "00000000-0000-0000-0000-00000000010d",
"cbd_mg_per_unit": "1.1",
"cbd_percentage": "2.2",
"is_primary": true,
"lab_license_number": "1234567890",
"lab_name": "Test Lab",
"mg_per_unit_type": "mg/g",
"name": "Name",
"release_date": "2025-05-22",
"thc_mg_per_unit": "3.3",
"thc_percentage": "4.4",
"total_cbd_mg_per_unit": "5.5",
"total_cbd_percentage": "6.6",
"total_thc_mg_per_unit": "7.7",
"total_thc_percentage": "8.8"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: e6ace22cb7a4d5e62ca91d43f4d0a7b3-94aebd7c4c6b2537-0
{
"data": {
"additional_test_results": {
"delta_8_thc_mg_per_unit": "17.3333",
"delta_8_thc_percentage": "12.55",
"thcva_percentage": "100"
},
"batch_id": "00000000-0000-0000-0000-00000000010d",
"biotrack_id": null,
"cbd_mg_per_unit": "1.1",
"cbd_percentage": "2.2",
"coa_url": null,
"id": "00000000-0000-0000-0000-00000000000e",
"inserted_datetime": "2026-08-24T16:01:08.788239Z",
"is_primary": true,
"lab_license_number": "1234567890",
"lab_name": "Test Lab",
"metrc_id": null,
"mg_per_unit_type": "mg/g",
"name": "Name",
"package_id": null,
"release_date": "2025-05-22",
"thc_mg_per_unit": "3.3",
"thc_percentage": "4.4",
"total_cbd_mg_per_unit": "5.5",
"total_cbd_percentage": "6.6",
"total_thc_mg_per_unit": "7.7",
"total_thc_percentage": "8.8",
"updated_datetime": "2026-08-24T16:01:08.788239Z"
}
}
Error scenario: invalid mg_per_unit_type
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 024f7de4999668d4752633851724bc4b-55d4a87da3ce2048-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"mg_per_unit_type"
],
"section": "body"
}
]
}
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.
On 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.
Updates 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.
Results 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.
Effects beyond the test result row:
• Creating a result attaches it to the given package or batch and propagates it down to that material's child packages where applicable.
• 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.
• 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.
Required permission: products_permissions_edit.
Request
POST /public/v1/test-results
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| additional_test_results | 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 for the valid options. |
body | object | false | {} | |
| batch_id | 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. |
body | string | false | 123e4567-e89b-12d3-a456-426614174000 | |
| cbd_mg_per_unit | 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. |
body | decimal | false | 1.5 | |
| cbd_percentage | 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. |
body | decimal | false | 1.5 | |
| id | 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. |
body | string | false | ||
| is_primary | 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. |
body | boolean | false | true | |
| lab_license_number | The license number of the testing lab. Optional; omitting it on update leaves it unchanged. Send null to clear it. |
body | string | false | 1234567890 | |
| lab_name | The name of the testing lab. Optional; omitting it on update leaves it unchanged. Send null to clear it. |
body | string | false | Lab Name | |
| mg_per_unit_type | 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.mg/g mg/mL |
body | string | false | mg/g | |
| name | Required on create; on update, omit to leave unchanged. A display name for this result (250 characters or fewer). | body | string | false | Test Result Name | |
| package_id | 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). |
body | string | false | 123e4567-e89b-12d3-a456-426614174000 | |
| release_date | 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. |
body | string | false | 2022-07-10 | |
| thc_mg_per_unit | 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. |
body | decimal | false | 1.5 | |
| thc_percentage | 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. |
body | decimal | false | 1.5 | |
| total_cbd_mg_per_unit | 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. |
body | decimal | false | 1.5 | |
| total_cbd_percentage | 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. |
body | decimal | false | 1.5 | |
| total_thc_mg_per_unit | 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. |
body | decimal | false | 1.5 | |
| total_thc_percentage | 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. |
body | decimal | false | 1.5 |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single test result | TestResultResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
UnitType
Get a unit type
Success scenario
GET /public/v1/unit-types/00000000-0000-0000-0000-000000000f79
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiNTlkYjYwNWEtOTcxYi00ZWIwLWFjYjEtN2NlYjBmNTU1OGI3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6Mzc5IiwidHlwIjoiYWNjZXNzIn0.OhWcWjvDhJ7ONVFV6BRrDd6g-dIZ64QYZfZw-M4F350
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: d7d4294454519e591cb0b19a68a9447e-ea81ed08a1cec549-0
{
"data": {
"active": true,
"category": "WEIGHT",
"id": "00000000-0000-0000-0000-000000000f79",
"inserted_datetime": "2026-08-24T16:01:04.103191Z",
"locked": true,
"name": "Big Bag",
"qty_per_si_unit": "453.592",
"updated_datetime": "2026-08-24T16:01:04.103191Z"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: cd8f906a8bd6a9967b16dbfda8d2118f-7926aa64dc42c8b1-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get a single unit type by ID. Returns 404 if no unit type with that ID exists in your company. Inactive unit types are still returned. This endpoint is read-only.
Remember the two shapes a unit type can take: a locked, built-in standard unit carries a
category and qty_per_si_unit; a custom unit you defined returns null for both.
Request
GET /public/v1/unit-types/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | ID of the unit type to fetch, as returned in the id field of the list endpoint. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single unit type | UnitTypeFullResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get unit types
Success scenario
GET /public/v1/unit-types
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiMzVlMjkzYWEtNDZiOC00MjdiLWJiMjgtYWUxMjZmODQ3OTg5IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTQ3IiwidHlwIjoiYWNjZXNzIn0.xCAeiVBaTZNxvS-Qexd3WGA1IBbaGxLskXWVeYBMgRs
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 16bca4f49051ebc18dcddcee012550b9-0ee3f7a46f830447-0
{
"data": [
{
"active": false,
"category": "WEIGHT",
"id": "00000000-0000-0000-0000-00000000059f",
"inserted_datetime": "2026-08-24T16:01:02.971862Z",
"locked": true,
"name": "Kilogram",
"qty_per_si_unit": "1",
"updated_datetime": "2026-08-24T16:01:02.971862Z"
},
{
"active": true,
"category": "WEIGHT",
"id": "00000000-0000-0000-0000-0000000005a0",
"inserted_datetime": "2026-08-24T16:01:02.971862Z",
"locked": true,
"name": "Gram",
"qty_per_si_unit": "1000",
"updated_datetime": "2026-08-24T16:01:02.971862Z"
},
{
"active": false,
"category": "WEIGHT",
"id": "00000000-0000-0000-0000-0000000005a1",
"inserted_datetime": "2026-08-24T16:01:02.971862Z",
"locked": true,
"name": "Milligram",
"qty_per_si_unit": "1000000",
"updated_datetime": "2026-08-24T16:01:02.971862Z"
}
],
"next_page": "https://www.example.com/public/v1/unit-types?page[number]=2"
}
Error scenario: invalid page param
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 9414b0e2ebc5051aceb30b04ea0a6678-39b1c6ddaa69e01f-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
List unit types for the authenticated company. A unit type is a company-scoped unit of measure (e.g. Gram, Pound, Milliliter, or a custom unit you define) used to express quantities throughout Distru — on products, inventory levels, cost type rates, and order and purchase line items. This endpoint is read-only; unit types are managed in the Distru web app.
There are two kinds of unit type in the response:
• Distru's built-in standard units (Gram, Pound, Liter, etc.) are locked (cannot be renamed or
deleted) and always carry a category (COUNT, VOLUME, or WEIGHT) and a qty_per_si_unit
conversion factor.
• Custom units you create are unlocked and have both category and qty_per_si_unit set to null —
they are treated as opaque labels with no physical conversion.
Results are scoped to your company and ordered oldest-first by creation time. Both active and
inactive unit types are returned. The response is paginated — follow the next_page URL to page
through the full set.
Request
GET /public/v1/unit-types
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of unit types | UnitTypes |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
User
Get a user
Success scenario
GET /public/v1/users/00000000-0000-0000-0000-000000000295
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjUsImlhdCI6MTc4NzU4NzI2NSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYjc1ZWQwOTctY2QzNy00N2NhLTk5OWMtNmEyOWY2YjI3NjA3IiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjY0LCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NjU0IiwidHlwIjoiYWNjZXNzIn0.aqQpIuARgzuFf6TZDMGFsg2RGGJQ06WTG4-G0lfAVeI
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 38df6c4877175865a234e3052e62063e-4e8c2885f985f5b7-0
{
"data": {
"banned": false,
"deleted_at": null,
"email": "owner-658@example.com",
"full_name": "FirstName1347 LastName1348",
"id": "00000000-0000-0000-0000-000000000295",
"inserted_datetime": "2026-08-24T16:01:05.169987Z",
"role": {
"id": "00000000-0000-0000-0000-0000000002a5",
"name": "Admin 676"
}
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 6b18eae17e4513a5e9bc7f743d62469c-2672548c9ba69187-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
Get a single team member (user) by their Distru user ID, scoped to your company.
This 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.
Like the list endpoint, this data is eventually consistent and may lag a recent create, update, or delete by up to ~1 second.
Required permission: settings_permissions_manage_team.
Request
GET /public/v1/users/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The user's Distru ID, as returned in the id field of the users list. |
path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single user | UserResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get users
Success scenario
GET /public/v1/users
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjMsImlhdCI6MTc4NzU4NzI2MywiaXNzIjoiRGlzdHJ1IiwianRpIjoiYWZmZGJjMDktZjYwMi00MTY4LTk3MTgtZDljYjk1N2QyOThhIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYyLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MjQ0IiwidHlwIjoiYWNjZXNzIn0.cXonN7pQ3XLpR6UmD1y7fGXahwteREtGVpXHnyfoP7Q
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 2057f8333967ca283360e35af3d013cb-9a0de33f5d10ad7a-0
{
"data": [
{
"banned": false,
"deleted_at": null,
"email": "owner-243@example.com",
"full_name": "FirstName504 LastName505",
"id": "00000000-0000-0000-0000-0000000000f4",
"inserted_datetime": "2026-08-24T16:01:03.623535Z",
"role": {
"id": "00000000-0000-0000-0000-0000000000f6",
"name": "Admin 245"
}
},
{
"banned": false,
"deleted_at": null,
"email": "owner-244@example.com",
"full_name": "FirstName506 LastName507",
"id": "00000000-0000-0000-0000-0000000000f5",
"inserted_datetime": "2026-08-24T16:01:03.633135Z",
"role": {
"id": "00000000-0000-0000-0000-0000000000f7",
"name": "Admin 246"
}
}
],
"next_page": null
}
Error scenario: invalid page number
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 220d44b6dc16122be965553f7e3b342f-96d7b68311c569eb-0
{
"errors": [
{
"context": {},
"message": "must be greater than 0",
"pointer": [
"page",
"number"
],
"section": "query"
}
]
}
Get a paginated list of the team members (users) in your company, sorted oldest-first by their creation datetime.
Use 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.
This 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.
The 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.
Required permission: settings_permissions_manage_team.
Request
GET /public/v1/users
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| deleted | 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.no include only |
query | string | false | no | |
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | 2022-07-10T00:00:00Z, | |
| page | 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. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ,2022-07-10T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of users | Users |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Vehicle
Create or update a vehicle
Success scenario
POST /public/v1/vehicles
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjQsImlhdCI6MTc4NzU4NzI2NCwiaXNzIjoiRGlzdHJ1IiwianRpIjoiMzJlMTI1NmItNjhkZC00ZGM2LTliYzEtNGU4N2E1MTVhOGYzIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYzLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MzcxIiwidHlwIjoiYWNjZXNzIn0.y-HeCI-_HBUFDkoduLqNW75jWadDGZEwB3SEWw85suw
{
"color": "Red",
"description": "Delivery truck",
"license_plate_number": "XYZ789",
"license_plate_state": "TX",
"make": "Ford",
"model": "F-150",
"vin": "ABCDEFGHIJ1234567",
"year": "2024"
}
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f2e622bc775a045230c41aaa4eb4fcf3-7a831707ea96aaf7-0
{
"data": {
"color": "Red",
"description": "Delivery truck",
"id": "00000000-0000-0000-0000-000000000008",
"inserted_datetime": "2026-08-24T16:01:04.111870Z",
"license_plate_number": "XYZ789",
"license_plate_state": "TX",
"make": "Ford",
"model": "F-150",
"updated_datetime": "2026-08-24T16:01:04.111870Z",
"vin": "ABCDEFGHIJ1234567",
"year": "2024"
}
}
Error scenario: missing required fields
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: f17556cc99ac55bbbc6e1c7fc8b68004-facfc70cc672d4e1-0
{
"errors": [
{
"context": {},
"message": "can't be blank",
"pointer": [
"license_plate_number"
],
"section": "body"
},
{
"context": {},
"message": "can't be blank",
"pointer": [
"make"
],
"section": "body"
},
{
"context": {},
"message": "can't be blank",
"pointer": [
"model"
],
"section": "body"
}
]
}
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.
On 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.
If 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.
On 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.
Vehicles are a settings-level record referenced by transfers and manifests; upserting one does not touch inventory, orders, or purchases.
Required permission: settings_permissions_vehicles.
Request
POST /public/v1/vehicles
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| color | Color of the vehicle (e.g. "White"). Optional, but required when your company has BioTrack enabled. Omit to leave unchanged on update. | body | string | false | ||
| description | Free-text name or description for the vehicle. Optional, but required when your company has BioTrack enabled. Omit to leave unchanged on update. | body | string | false | ||
| id | ID of the vehicle to update. Omit to create a new vehicle; when present it must identify a vehicle owned by your company. | body | string | false | ||
| license_plate_number | License plate number. Required on create and can never be cleared; on update, omit to leave unchanged. | body | string | false | ||
| license_plate_state | 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. | body | string | false | ||
| make | Manufacturer of the vehicle (e.g. "Ford"). Required on create and can never be cleared; on update, omit to leave unchanged. | body | string | false | ||
| model | Model of the vehicle (e.g. "Transit"). Required on create and can never be cleared; on update, omit to leave unchanged. | body | string | false | ||
| vin | Vehicle identification number (VIN). Optional, but required when your company has BioTrack enabled. Omit to leave unchanged on update. | body | string | false | ||
| year | 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. | body | string | false |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | The created or updated vehicle | VehicleResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Delete a vehicle
Success scenario
DELETE /public/v1/vehicles/00000000-0000-0000-0000-000000000001
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjEsImlhdCI6MTc4NzU4NzI2MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTg2ZTEyZjktOGFhMC00Y2JjLTg1NTAtZmI5ZWZkZjAyNjBmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTEiLCJ0eXAiOiJhY2Nlc3MifQ.WtsHqQmn_D9xPOk5h5_DDrrnvEEJr28IO6q_3f0ofY4
Response
204
cache-control: max-age=0, private, must-revalidate
b3: 5448ba9743f5da802811d6c7d10286db-5a5edcd33594b7bb-0
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: ba814631d8868f824545ba38e52cf1fa-ff618aa77fb9cfae-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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).
A 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.
For 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.
Required permission: settings_permissions_vehicles.
Request
DELETE /public/v1/vehicles/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | 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. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 204 | No Content | |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get a vehicle
Success scenario
GET /public/v1/vehicles/00000000-0000-0000-0000-000000000001
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjEsImlhdCI6MTc4NzU4NzI2MSwiaXNzIjoiRGlzdHJ1IiwianRpIjoiOTg2ZTEyZjktOGFhMC00Y2JjLTg1NTAtZmI5ZWZkZjAyNjBmIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYwLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6MTEiLCJ0eXAiOiJhY2Nlc3MifQ.WtsHqQmn_D9xPOk5h5_DDrrnvEEJr28IO6q_3f0ofY4
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 5b5b7edc421815b6e2596c872ecb2523-db1fbeec30132f9a-0
{
"data": {
"color": "Blue",
"description": "Company car",
"id": "00000000-0000-0000-0000-000000000001",
"inserted_datetime": "2026-08-24T16:01:01.618999Z",
"license_plate_number": "ABC123",
"license_plate_state": "CA",
"make": "Toyota",
"model": "Camry",
"updated_datetime": "2026-08-24T16:01:01.618999Z",
"vin": "1234567890",
"year": "2023"
}
}
Error scenario: invalid id
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: a882711585fc14e450ac05eda7e59e63-84f2d7238fe45865-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"id"
],
"section": "path"
}
]
}
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.
Required permission: settings_permissions_vehicles.
Request
GET /public/v1/vehicles/{id}
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| id | The vehicle's id, as returned by the list or upsert endpoints. | path | string | true |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A single vehicle | VehicleResponse |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
| 404 | Not Found | ErrorResponse |
Get vehicles
Success scenario
GET /public/v1/vehicles
content-type: application/json
accept: application/json
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJEaXN0cnUiLCJleHAiOjE4MTkwMzY4NjIsImlhdCI6MTc4NzU4NzI2MiwiaXNzIjoiRGlzdHJ1IiwianRpIjoiYmM4MDQ4YTItMjllZC00OWIyLThhZDktNWM0ZmIzMjI1YzhkIiwibW9iaWxlIjpmYWxzZSwibmJmIjoxNzg3NTg3MjYxLCJwbGF0Zm9ybSI6IkFQSSIsInN1YiI6IlVzZXI6NzciLCJ0eXAiOiJhY2Nlc3MifQ.xbOz_ymv0al4lQYQL2vVW-VaJiYlzHBaOKfsQcqnK1Q
Response
200
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: 4a46073da8a2c50e82781de4b6b8b6e1-9f363b12bfe9095d-0
{
"data": [
{
"color": "Red",
"description": "Test Vehicle",
"id": "00000000-0000-0000-0000-000000000002",
"inserted_datetime": "2026-08-24T16:01:02.383736Z",
"license_plate_number": "1234567890ABCDEFG",
"license_plate_state": "CA",
"make": "Toyota",
"model": "Camry",
"updated_datetime": "2026-08-24T16:01:02.383736Z",
"vin": "1234567890ABCDEFG",
"year": "2020"
},
{
"color": "Red",
"description": "Test Vehicle",
"id": "00000000-0000-0000-0000-000000000003",
"inserted_datetime": "2026-08-24T16:01:02.390678Z",
"license_plate_number": "1234567890ABCDEFG",
"license_plate_state": "CA",
"make": "Honda",
"model": "Civic",
"updated_datetime": "2026-08-24T16:01:02.390678Z",
"vin": "1234567890ABCDEFG",
"year": "2020"
}
],
"next_page": null
}
Error scenario: invalid page param
Response
400
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
b3: df335c9a43b5fad6e1e88900bd49aced-8c65f06f5749e9ea-0
{
"errors": [
{
"context": {},
"message": "is invalid",
"pointer": [
"page"
],
"section": "query"
}
]
}
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.
Results are paged; follow next_page in the response envelope until it is null to walk every page.
Required permission: settings_permissions_vehicles.
Request
GET /public/v1/vehicles
Parameters
| Parameter | Description | In | Type | Required | Default | Example |
|---|---|---|---|---|---|---|
| ids | 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. |
query | array | false | ?ids[]=550e8400-e29b-41d4-a716-446655440000&ids[]=6ba7b810-9dad-11d1-80b4-00c04fd430c8 | |
| inserted_datetime | 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. |
query | string | false | ?inserted_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z | |
| page | 1-based page number to fetch. Defaults to page 1 when omitted; must be greater than 0. Example: ?page[number]=2. |
query | number | false | ?page[number]=1 | |
| updated_datetime | 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. |
query | string | false | ?updated_datetime=2022-07-10T00:00:00Z,2022-07-31T00:00:00Z |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | A list of vehicles | Vehicles |
| 400 | Invalid parameters | ErrorResponse |
| 401 | Missing or invalid API token | ErrorResponse |
| 403 | The API token lacks the required permission | ErrorResponse |
Models
AddBatchCostsRequest
| Property | Description | Type | Required |
|---|---|---|---|
| batch_ids | 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 | array(any) | true |
| costs | Required. Non-empty list of costs; each entry is applied to every listed batch | array(CostEntryInput) | true |
| distribute_by_quantity | 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 | boolean | false |
| location_ids | 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 | array(any) | false |
AddPackageCostsRequest
| Property | Description | Type | Required |
|---|---|---|---|
| costs | Required. Non-empty list of costs; each entry is applied to every listed package | array(CostEntryInput) | true |
| distribute_by_quantity | 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 | boolean | false |
| package_ids | 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 | array(any) | true |
AddProductCostsRequest
| Property | Description | Type | Required |
|---|---|---|---|
| costs | Required. Non-empty list of costs; each entry is applied to every listed product | array(CostEntryInput) | true |
| distribute_by_quantity | 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 | boolean | false |
| location_ids | 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 | array(any) | false |
| product_ids | 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 | array(any) | true |
AdditionalTestResult
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.
| Property | Description | Type | Required |
|---|---|---|---|
| acephate_ug_per_g | Pesticide | string | false |
| acequinocyl_ug_per_g | Pesticide | string | false |
| acetamiprid_ug_per_g | Pesticide | string | false |
| acetic_acid_percentage | Other | string | false |
| acetic_acid_ug_per_g | Other | string | false |
| acetone_ug_per_g | Solvent | string | false |
| acetonitrile_ug_per_g | Solvent | string | false |
| aflatoxin_b1_ug_per_kg | Mycotoxin | string | false |
| aflatoxin_b2_ug_per_kg | Mycotoxin | string | false |
| aflatoxin_g1_ug_per_kg | Mycotoxin | string | false |
| aflatoxin_g2_ug_per_kg | Mycotoxin | string | false |
| aflatoxins_ug_per_kg | Mycotoxin | string | false |
| aldicarb_ug_per_g | Pesticide | string | false |
| alpha_bisabolol_mg_per_unit | Terpene | string | false |
| alpha_bisabolol_percentage | Terpene | string | false |
| alpha_cyfluthrin_ug_per_g | Pesticide | string | false |
| alpha_cypermethrin_ug_per_g | Pesticide | string | false |
| alpha_humulene_mg_per_unit | Terpene | string | false |
| alpha_humulene_percentage | Terpene | string | false |
| alpha_myrcene_mg_per_unit | Terpene | string | false |
| alpha_myrcene_percentage | Terpene | string | false |
| alpha_phellandrene_mg_per_unit | Terpene | string | false |
| alpha_phellandrene_percentage | Terpene | string | false |
| alpha_pinene_mg_per_unit | Terpene | string | false |
| alpha_pinene_percentage | Terpene | string | false |
| alpha_terpinene_mg_per_unit | Terpene | string | false |
| alpha_terpinene_percentage | Terpene | string | false |
| ancymidol_ug_per_g | Pesticide | string | false |
| antimony_ug_per_g | Heavy Metal | string | false |
| arsenic_ug_per_g | Heavy Metal | string | false |
| aspergillus_cfu_per_g | Microbial | string | false |
| aspergillus_flavus_cfu_per_g | Microbial | string | false |
| aspergillus_fumigatus_cfu_per_g | Microbial | string | false |
| aspergillus_niger_cfu_per_g | Microbial | string | false |
| aspergillus_terreus_cfu_per_g | Microbial | string | false |
| azoxystrobin_ug_per_g | Pesticide | string | false |
| benzene_ug_per_g | Solvent | string | false |
| beta_caryophyllene_mg_per_unit | Terpene | string | false |
| beta_caryophyllene_percentage | Terpene | string | false |
| beta_cyfluthrin_ug_per_g | Pesticide | string | false |
| beta_cypermethrin_ug_per_g | Pesticide | string | false |
| beta_humulene_mg_per_unit | Terpene | string | false |
| beta_humulene_percentage | Terpene | string | false |
| beta_myrcene_mg_per_unit | Terpene | string | false |
| beta_myrcene_percentage | Terpene | string | false |
| beta_pinene_mg_per_unit | Terpene | string | false |
| beta_pinene_percentage | Terpene | string | false |
| bifenazate_ug_per_g | Pesticide | string | false |
| bifenthrin_ug_per_g | Pesticide | string | false |
| borneol_mg_per_unit | Terpene | string | false |
| borneol_percentage | Terpene | string | false |
| boscalid_ug_per_g | Pesticide | string | false |
| butane_ug_per_g | Solvent | string | false |
| butanol_ug_per_g | Solvent | string | false |
| butyl_acetate_ug_per_g | Solvent | string | false |
| cadmium_ug_per_g | Heavy Metal | string | false |
| camphene_mg_per_unit | Terpene | string | false |
| camphene_percentage | Terpene | string | false |
| camphor_mg_per_unit | Terpene | string | false |
| camphor_percentage | Terpene | string | false |
| candida_albicans_cfu_per_g | Microbial | string | false |
| cannabinoids_mg_per_unit_total | Cannabinoid | string | false |
| cannabinoids_percentage_total | Cannabinoid | string | false |
| captan_ug_per_g | Pesticide | string | false |
| carbaryl_ug_per_g | Pesticide | string | false |
| carbofuran_ug_per_g | Pesticide | string | false |
| caryophyllene_oxide_mg_per_unit | Terpene | string | false |
| caryophyllene_oxide_percentage | Terpene | string | false |
| cbc_mg_per_unit | Cannabinoid | string | false |
| cbc_percentage | Cannabinoid | string | false |
| cbca_mg_per_unit | Cannabinoid | string | false |
| cbca_percentage | Cannabinoid | string | false |
| cbda_mg_per_unit | Cannabinoid | string | false |
| cbda_percentage | Cannabinoid | string | false |
| cbdv_mg_per_unit | Cannabinoid | string | false |
| cbdv_percentage | Cannabinoid | string | false |
| cbg_mg_per_unit | Cannabinoid | string | false |
| cbg_percentage | Cannabinoid | string | false |
| cbga_mg_per_unit | Cannabinoid | string | false |
| cbga_percentage | Cannabinoid | string | false |
| cbl_mg_per_unit | Cannabinoid | string | false |
| cbl_percentage | Cannabinoid | string | false |
| cbn_mg_per_unit | Cannabinoid | string | false |
| cbn_percentage | Cannabinoid | string | false |
| cbt_mg_per_unit | Cannabinoid | string | false |
| cbt_percentage | Cannabinoid | string | false |
| chlorantraniliprole_ug_per_g | Pesticide | string | false |
| chlordane_cis_ug_per_g | Pesticide | string | false |
| chlordane_trans_ug_per_g | Pesticide | string | false |
| chlordane_ug_per_g | Pesticide | string | false |
| chlorfenapyr_ug_per_g | Pesticide | string | false |
| chlormequat_chloride_percentage | Other | string | false |
| chlormequat_chloride_ug_per_g | Other | string | false |
| chlorobenzene_ug_per_g | Solvent | string | false |
| chloroform_ug_per_g | Solvent | string | false |
| chlorpyrifos_ug_per_g | Pesticide | string | false |
| chromium_ug_per_g | Heavy Metal | string | false |
| clofentezine_ug_per_g | Pesticide | string | false |
| clothianidin_ug_per_g | Pesticide | string | false |
| copper_ug_per_g | Heavy Metal | string | false |
| coumaphos_ug_per_g | Pesticide | string | false |
| cumene_ug_per_g | Solvent | string | false |
| cyclohexane_ug_per_g | Solvent | string | false |
| cyfluthrin_ug_per_g | Pesticide | string | false |
| cymene_mg_per_unit | Terpene | string | false |
| cymene_percentage | Terpene | string | false |
| cypermethrin_ug_per_g | Pesticide | string | false |
| daminozide_ug_per_g | Pesticide | string | false |
| delta_3_carene_mg_per_unit | Terpene | string | false |
| delta_3_carene_percentage | Terpene | string | false |
| delta_8_thc_mg_per_unit | Cannabinoid | string | false |
| delta_8_thc_percentage | Cannabinoid | string | false |
| diazinon_ug_per_g | Pesticide | string | false |
| dichloroethane_ug_per_g | Solvent | string | false |
| dichloromethane_ug_per_g | Solvent | string | false |
| dichlorvos_ug_per_g | Pesticide | string | false |
| dimethoate_ug_per_g | Pesticide | string | false |
| dimethomorph_e_ug_per_g | Pesticide | string | false |
| dimethomorph_ug_per_g | Pesticide | string | false |
| dimethomorph_z_ug_per_g | Pesticide | string | false |
| dimethoxyethane_ug_per_g | Solvent | string | false |
| dimethyl_sulfoxide_ug_per_g | Solvent | string | false |
| dimethylacetamide_ug_per_g | Solvent | string | false |
| dimethylformamide_ug_per_g | Solvent | string | false |
| dinotefuran_ug_per_g | Pesticide | string | false |
| dioxane_ug_per_g | Solvent | string | false |
| diuron_ug_per_g | Pesticide | string | false |
| e_coli_cfu_per_g | Microbial | string | false |
| enterobacteriacaea_cfu_per_g | Microbial | string | false |
| ethanol_ug_per_g | Solvent | string | false |
| ethephon_ug_per_g | Pesticide | string | false |
| ethoprophos_ug_per_g | Pesticide | string | false |
| ethoxyethanol_ug_per_g | Solvent | string | false |
| ethyl_acetate_ug_per_g | Solvent | string | false |
| ethyl_ether_ug_per_g | Solvent | string | false |
| ethyl_formate_percentage | Other | string | false |
| ethyl_formate_ug_per_g | Other | string | false |
| ethylene_glycol_percentage | Other | string | false |
| ethylene_glycol_ug_per_g | Other | string | false |
| ethylene_oxide_ug_per_g | Solvent | string | false |
| etofenprox_ug_per_g | Pesticide | string | false |
| etoxazole_ug_per_g | Pesticide | string | false |
| eucalyptol_mg_per_unit | Terpene | string | false |
| eucalyptol_percentage | Terpene | string | false |
| farnesene_mg_per_unit | Terpene | string | false |
| farnesene_percentage | Terpene | string | false |
| fenchol_mg_per_unit | Terpene | string | false |
| fenchol_percentage | Terpene | string | false |
| fenhexamid_ug_per_g | Pesticide | string | false |
| fenoxycarb_ug_per_g | Pesticide | string | false |
| fenpyroximate_ug_per_g | Pesticide | string | false |
| filth_and_foreign_material_percentage | Other | string | false |
| fipronil_ug_per_g | Pesticide | string | false |
| flonicamid_ug_per_g | Pesticide | string | false |
| fludioxonil_ug_per_g | Pesticide | string | false |
| flurprimidol_ug_per_g | Pesticide | string | false |
| formamide_ug_per_g | Pesticide | string | false |
| formic_acid_percentage | Other | string | false |
| formic_acid_ug_per_g | Other | string | false |
| gamma_terpinene_mg_per_unit | Terpene | string | false |
| gamma_terpinene_percentage | Terpene | string | false |
| geraniol_mg_per_unit | Terpene | string | false |
| geraniol_percentage | Terpene | string | false |
| guaiol_mg_per_unit | Terpene | string | false |
| guaiol_percentage | Terpene | string | false |
| heptane_ug_per_g | Solvent | string | false |
| hexane_ug_per_g | Solvent | string | false |
| hexythiazox_ug_per_g | Pesticide | string | false |
| imazalil_ug_per_g | Pesticide | string | false |
| imidacloprid_ug_per_g | Pesticide | string | false |
| isobutyl_acetate_ug_per_g | Solvent | string | false |
| isopropanol_ug_per_g | Solvent | string | false |
| isopropyl_acetate_ug_per_g | Solvent | string | false |
| isopulegol_mg_per_unit | Terpene | string | false |
| isopulegol_percentage | Terpene | string | false |
| kresoxim_methyl_ug_per_g | Pesticide | string | false |
| l_monocytogenes_cfu_per_g | Microbial | string | false |
| lambda_cyhalothrin_ug_per_g | Pesticide | string | false |
| lead_ug_per_g | Heavy Metal | string | false |
| limonene_mg_per_unit | Terpene | string | false |
| limonene_percentage | Terpene | string | false |
| linalool_mg_per_unit | Terpene | string | false |
| linalool_percentage | Terpene | string | false |
| m_and_p_xylene_ug_per_g | Solvent | string | false |
| malathion_ug_per_g | Pesticide | string | false |
| mercury_ug_per_g | Heavy Metal | string | false |
| metalaxyl_ug_per_g | Pesticide | string | false |
| methanol_ug_per_g | Solvent | string | false |
| methiocarb_ug_per_g | Pesticide | string | false |
| methomyl_ug_per_g | Pesticide | string | false |
| methoxybenzene_ug_per_g | Solvent | string | false |
| methoxyethanol_ug_per_g | Solvent | string | false |
| methyl_acetate_ug_per_g | Solvent | string | false |
| methyl_butanol_ug_per_g | Solvent | string | false |
| methyl_butyl_ketone_ug_per_g | Solvent | string | false |
| methyl_ethyl_ketone_ug_per_g | Solvent | string | false |
| methyl_parathion_ug_per_g | Pesticide | string | false |
| methyl_propanol_ug_per_g | Solvent | string | false |
| methylcyclohexane_ug_per_g | Solvent | string | false |
| methylisobutyl_ketone_ug_per_g | Solvent | string | false |
| mevinphos_i_ug_per_g | Pesticide | string | false |
| mevinphos_ii_ug_per_g | Pesticide | string | false |
| mevinphos_ug_per_g | Pesticide | string | false |
| mgk_264_ug_per_g | Pesticide | string | false |
| moisture_percentage | Moisture | string | false |
| mold_cfu_per_g | Microbial | string | false |
| myclobutanil_ug_per_g | Pesticide | string | false |
| n_methylpyrrolidone_ug_per_g | Solvent | string | false |
| naled_ug_per_g | Pesticide | string | false |
| nerolidol_mg_per_unit | Terpene | string | false |
| nerolidol_percentage | Terpene | string | false |
| nickel_ug_per_g | Heavy Metal | string | false |
| nitromethane_ug_per_g | Solvent | string | false |
| ochratoxin_a_ug_per_kg | Mycotoxin | string | false |
| ocimene_mg_per_unit | Terpene | string | false |
| ocimene_percentage | Terpene | string | false |
| other_heavy_metals_ug_per_g | Heavy Metal | string | false |
| other_microbials_cfu_per_g | Microbial | string | false |
| other_mycotoxins_ug_per_kg | Mycotoxin | string | false |
| other_pesticides_ug_per_g | Pesticide | string | false |
| other_solvents_ug_per_g | Solvent | string | false |
| other_terpenes_mg_per_unit | Terpene | string | false |
| other_terpenes_percentage | Terpene | string | false |
| oxamyl_ug_per_g | Pesticide | string | false |
| paclobutrazol_ug_per_g | Pesticide | string | false |
| pentachloronitrobenzene_ug_per_g | Pesticide | string | false |
| pentane_ug_per_g | Solvent | string | false |
| pentanol_ug_per_g | Solvent | string | false |
| permethrin_cis_ug_per_g | Pesticide | string | false |
| permethrin_trans_ug_per_g | Pesticide | string | false |
| permethrin_ug_per_g | Pesticide | string | false |
| phosmet_ug_per_g | Pesticide | string | false |
| phytol_mg_per_unit | Terpene | string | false |
| phytol_percentage | Terpene | string | false |
| piperonylbutoxide_ug_per_g | Pesticide | string | false |
| prallethrin_cis_ug_per_g | Pesticide | string | false |
| prallethrin_trans_ug_per_g | Pesticide | string | false |
| prallethrin_ug_per_g | Pesticide | string | false |
| propane_ug_per_g | Solvent | string | false |
| propanol_ug_per_g | Solvent | string | false |
| propiconazole_cis_ug_per_g | Pesticide | string | false |
| propiconazole_trans_ug_per_g | Pesticide | string | false |
| propiconazole_ug_per_g | Pesticide | string | false |
| propoxur_ug_per_g | Pesticide | string | false |
| propyl_acetate_ug_per_g | Solvent | string | false |
| pulegone_mg_per_unit | Terpene | string | false |
| pulegone_percentage | Terpene | string | false |
| pyrethrins_cinerin_i_ug_per_g | Pesticide | string | false |
| pyrethrins_cinerin_ii_ug_per_g | Pesticide | string | false |
| pyrethrins_jasmolin_i_ug_per_g | Pesticide | string | false |
| pyrethrins_jasmolin_ii_ug_per_g | Pesticide | string | false |
| pyrethrins_pyrethrin_i_ug_per_g | Pesticide | string | false |
| pyrethrins_pyrethrin_ii_ug_per_g | Pesticide | string | false |
| pyrethrins_ug_per_g | Pesticide | string | false |
| pyridaben_ug_per_g | Pesticide | string | false |
| pyridine_ug_per_g | Solvent | string | false |
| pyriproxyfen_ug_per_g | Pesticide | string | false |
| sabinene_mg_per_unit | Terpene | string | false |
| sabinene_percentage | Terpene | string | false |
| salmonella_cfu_per_g | Microbial | string | false |
| sand_and_soil_and_cinders_and_dirt_percentage | Other | string | false |
| spinetoram_j_ug_per_g | Pesticide | string | false |
| spinetoram_l_ug_per_g | Pesticide | string | false |
| spinetoram_ug_per_g | Pesticide | string | false |
| spinosad_a_ug_per_g | Pesticide | string | false |
| spinosad_d_ug_per_g | Pesticide | string | false |
| spinosad_ug_per_g | Pesticide | string | false |
| spiromesifen_ug_per_g | Pesticide | string | false |
| spirotetramat_ug_per_g | Pesticide | string | false |
| spiroxamine_a_ug_per_g | Pesticide | string | false |
| spiroxamine_b_ug_per_g | Pesticide | string | false |
| spiroxamine_ug_per_g | Pesticide | string | false |
| sulfolane_ug_per_g | Solvent | string | false |
| tebuconazole_ug_per_g | Pesticide | string | false |
| terpenes_mg_per_unit_total | Terpene | string | false |
| terpenes_percentage_total | Terpene | string | false |
| terpineol_mg_per_unit | Terpene | string | false |
| terpineol_percentage | Terpene | string | false |
| terpinolene_mg_per_unit | Terpene | string | false |
| terpinolene_percentage | Terpene | string | false |
| tert_butyl_methyl_ether_ug_per_g | Solvent | string | false |
| tetrahydrofuran_ug_per_g | Solvent | string | false |
| tetralin_ug_per_g | Solvent | string | false |
| thca_mg_per_unit | Cannabinoid | string | false |
| thca_percentage | Cannabinoid | string | false |
| thcv_mg_per_unit | Cannabinoid | string | false |
| thcv_percentage | Cannabinoid | string | false |
| thcva_mg_per_unit | Cannabinoid | string | false |
| thcva_percentage | Cannabinoid | string | false |
| thiabendazole_ug_per_g | Pesticide | string | false |
| thiacloprid_ug_per_g | Pesticide | string | false |
| thiamethoxam_ug_per_g | Pesticide | string | false |
| toluene_ug_per_g | Solvent | string | false |
| trichloroethylene_ug_per_g | Solvent | string | false |
| trifloxystrobin_ug_per_g | Pesticide | string | false |
| valencene_mg_per_unit | Terpene | string | false |
| valencene_percentage | Terpene | string | false |
| vitamin_e_acetate_percentage | Other | string | false |
| vitamin_e_acetate_ug_per_g | Other | string | false |
| water_activity_aw | Water Activity | string | false |
| xylene_ug_per_g_total | Solvent | string | false |
| yeast_cfu_per_g | Microbial | string | false |
| zinc_ug_per_g | Heavy Metal | string | false |
Assemblies
A collection of Assemblies
| Property | Description | Type | Required |
|---|---|---|---|
| data | Assemblies | array(Assembly) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Assembly
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.
| Property | Description | Type | Required |
|---|---|---|---|
| assembly_number | Human-readable reference number for this assembly, shown in Distru (e.g. "AS-0000001") | string | true |
| completion_datetime | ISO 8601 datetime the assembly was completed at, or null until its status becomes COMPLETED. |
string | false |
| compliance_type | Which state compliance system, if any, this assembly reports to. One of METRC, BIOTRACK, or NONE. | string | true |
| creation_source | How this assembly was created.
MANUALLY_CREATED SALES_ORDER SPLIT_PACKAGE LAB_TESTING |
string | true |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| custom_data | The custom data for this assembly | array(CustomField) | true |
| description | Free-text description of this assembly, or null when none was entered | string | false |
| estimated_start_datetime | ISO 8601 datetime the assembly is expected to start, or null when not set. | string | false |
| estimated_work_hours | 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. |
integer | false |
| estimated_work_minutes | 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. |
integer | false |
| fulfilled | True when every input across all outputs has been fulfilled with a batch or package, false otherwise. A precondition for completing the assembly. | boolean | true |
| id | ID for this assembly | string | true |
| inserted_datetime | The datetime this assembly was created at | string | true |
| license | A cannabis license held by a company or tied to a location, identifying it to the state and its compliance system. | License | false |
| metrc_processing_job | The Metrc processing job details for an assembly | AssemblyMetrcProcessingJob | false |
| outputs | The outputs for this assembly | array(AssemblyOutput) | true |
| owner_id | The ID of the Distru user who owns this assembly, or null when unassigned | string | false |
| status | Where this assembly is in its lifecycle.
PENDING COMPLETED |
string | true |
| updated_datetime | The datetime this assembly was last updated at | string | true |
AssemblyCost
A cost added directly to an assembly output (e.g. labor or packaging), as shown in Distru
| Property | Description | Type | Required |
|---|---|---|---|
| cost_per_unit | The per-unit rate applied, as a decimal string (e.g. "10.50"). Null when no rate is set. | string | false |
| description | Free-text description of this cost, or null when none was entered | string | false |
| id | ID for this assembly cost | string | true |
| name | The name of the assembly cost | string | true |
| quantity | 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". |
string | true |
| unit_type | 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. | UnitType | false |
AssemblyInput
An ingredient consumed by an assembly — the inventory used up to produce an output, and its cost.
| Property | Description | Type | Required |
|---|---|---|---|
| batch | 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. | Batch | false |
| compliance_quantity | 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. | string | false |
| cost_per_unit | Actual cost per unit — total_cost_actual divided by this input's quantity (in its product's unit). |
string | false |
| cost_per_unit_default | Default (standard) cost per unit — total_cost_default divided by this input's quantity (in its product's unit). |
string | false |
| id | ID for this assembly input | string | true |
| location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | false |
| package | 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. | Package | false |
| product | 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). |
Product | false |
| quantity | The quantity of this input in its product's unit, as a decimal string (e.g. "25"). | string | true |
| status | 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). | string | true |
| total_cost_actual | 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. | string | false |
| total_cost_default | 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. |
string | false |
AssemblyMetrcProcessingJob
The Metrc processing job details for an assembly
| Property | Description | Type | Required |
|---|---|---|---|
| id | 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. | integer | false |
| name | The processing job name reported to Metrc, or null when not set | string | false |
| notes | Free-text notes reported to Metrc for this processing job, or null when none | string | false |
| type_id | The Metrc processing job type ID selected for this job. A Metrc identifier, not a Distru ID. Null when not set. | integer | false |
| waste | Waste recorded when a Metrc processing job is finished | AssemblyMetrcProcessingJobWaste | false |
AssemblyMetrcProcessingJobWaste
Waste recorded when a Metrc processing job is finished
| Property | Description | Type | Required |
|---|---|---|---|
| count_quantity | Count-based waste, as a decimal string (e.g. "5"). Reported to Metrc as TotalCountWaste. Null when no count waste was recorded. | string | false |
| count_unit_name | The Metrc unit name for count_quantity (e.g. "Each"), or null when count_quantity is null |
string | false |
| volume_quantity | Volume-based waste, as a decimal string (e.g. "10.5"). Reported to Metrc as TotalVolumeWaste. Null when no volume waste was recorded. | string | false |
| volume_unit_name | The Metrc unit name for volume_quantity (e.g. "Milliliters"), or null when volume_quantity is null |
string | false |
| weight_quantity | Weight-based waste, as a decimal string (e.g. "2.75"). Reported to Metrc as TotalWeightWaste. Null when no weight waste was recorded. | string | false |
| weight_unit_name | The Metrc unit name for weight_quantity (e.g. "Grams"), or null when weight_quantity is null |
string | false |
AssemblyOutput
A finished product produced by an assembly, along with the quantity made and its cost.
| Property | Description | Type | Required |
|---|---|---|---|
| batch | 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. | Batch | false |
| batch_number | The batch number for this output, or null when none is set | string | false |
| bins | 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). | array(BinCompact) | true |
| compliance_label | The unique tag assigned by the state compliance system (the Metrc package tag or BioTrack barcode). Null when this output has no compliance tag. | string | false |
| compliance_quantity | 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. | string | false |
| copy_custom_data_from_input | True when this output copies its custom field values from its input rather than carrying its own. | boolean | false |
| cost_per_unit | Actual cost per unit — total_cost_actual divided by this output's quantity (in its product's unit). |
string | false |
| cost_per_unit_default | Default (standard) cost per unit — total_cost_default divided by this output's quantity (in its product's unit). |
string | false |
| costs | The costs added directly to this assembly output (e.g. labor or packaging), on top of the material cost carried over from its inputs | array(AssemblyCost) | true |
| expiration_date | The expiration date for this output (e.g. "2026-08-20"), or null when none is set | string | false |
| id | ID for this assembly output | string | true |
| inputs | 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). |
array(AssemblyInput) | true |
| is_donation | True when this output is marked as a donation. | boolean | true |
| is_finished_good | True if this output is a finished, sellable product (rather than an intermediate/work-in-progress item). Only applies to Metrc-tracked outputs. | boolean | true |
| is_production_batch | 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. | boolean | true |
| is_test_sample | True when this output is a test sample. | boolean | true |
| is_trade_sample | True when this output is a trade sample. | boolean | true |
| location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | false |
| metrc_item_id | The Metrc item id for this output. A Metrc identifier, not a Distru ID. Null when not applicable. | integer | false |
| metrc_location_id | The Metrc location id for this output. A Metrc identifier, not a Distru ID. Null when not applicable. | integer | false |
| metrc_notes | Notes recorded on this output that are sent to Metrc as the package's note when the package is created. Null when none. | string | false |
| metrc_production_batch_number | The Metrc production batch number, set only when this output is a production batch (see is_production_batch); otherwise null. |
string | false |
| package | 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. | Package | false |
| package_date | The date this output's package was created (e.g. "2026-08-20"), or null when not set | string | false |
| package_unit_type | 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. | UnitType | false |
| product | 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). |
Product | false |
| quantity | The quantity of this output in its product's unit, as a decimal string (e.g. "100"). | string | true |
| status | The status of this output. One of PENDING (not yet produced into inventory) or COMPLETED (produced, once the assembly is completed). | string | true |
| total_cost_actual | 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). |
string | false |
| total_cost_default | 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. |
string | false |
| use_same_item | True when this output reuses the source package's Metrc item rather than mapping to a new one. | boolean | true |
AssemblyResponse
A single assembly
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | Assembly | false |
Batch
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.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this batch | string | true |
| name | Human readable name for this batch | string | true |
BatchCompactWithQuantityActive
A compact batch reference plus its active on-hand quantity at the location it is nested under.
| Property | Description | Type | Required |
|---|---|---|---|
| batch_number | The batch number for this batch, or null when none is set | string | false |
| id | ID for this batch | string | true |
| name | Human readable name for this batch | string | true |
| quantity_active | The batch's active on-hand quantity at this location, as a decimal string (e.g. "100"). Always positive. | string | true |
BatchFull
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.
| Property | Description | Type | Required |
|---|---|---|---|
| batch_number | The batch number for this batch, or null when none is set | string | false |
| bins | The bins this batch is stored in. Only present when bin inventory tracking is enabled for the company. | array(BinCompact) | false |
| cbd | 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. |
string | false |
| cost_per_unit_actual | 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. |
string | false |
| cost_per_unit_default | 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. |
string | false |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| custom_data | The custom data for this batch | array(CustomField) | true |
| deleted_at | ISO 8601 datetime this batch was soft-deleted at, or null when the batch has not been deleted | string | false |
| description | Free-text description of this batch, or null when none was entered | string | false |
| expiration_datetime | ISO 8601 datetime this batch expires, or null when none is set. | string | false |
| harvest_datetime | ISO 8601 datetime this batch was harvested, or null when none is set | string | false |
| id | ID for this batch | string | true |
| inserted_datetime | The datetime this batch was created (ISO 8601) | string | true |
| manufactured_datetime | ISO 8601 datetime this batch was manufactured. Defaults to the batch's creation time when none is supplied, so it is always present. | string | true |
| name | Human readable name for this batch | string | true |
| owner_id | The ID of the Distru user who owns this batch, or null when unassigned | string | false |
| primary_test_result | A compact view of the primary test result 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. | PrimaryTestResult | false |
| product | 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). |
Product | true |
| product_id | The ID of the product this batch is a lot of | string | true |
| quantity_active | 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. |
string | true |
| quantity_active_by_location | 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. |
array(QuantityActiveByLocation) | true |
| thc | 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. |
string | false |
| total_cost_actual | 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. |
string | false |
| total_cost_default | 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. |
string | false |
| updated_datetime | The datetime this batch was last modified (ISO 8601) | string | true |
BatchFullResponse
A single batch
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | BatchFull | false |
Batches
A collection of Batches
| Property | Description | Type | Required |
|---|---|---|---|
| data | Batches | array(BatchFull) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
BatchesWithActiveQuantityByLocation
A location and the product's batches that hold active quantity there.
| Property | Description | Type | Required |
|---|---|---|---|
| batches | The product's batches with active quantity at this location, each carrying its active quantity there, ordered by id. | array(BatchCompactWithQuantityActive) | true |
| location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | true |
BillOfMaterials
A product's bill of materials (recipe of inputs and additional costs)
| Property | Description | Type | Required |
|---|---|---|---|
| costs | The additional (non-material) costs this recipe applies, ordered oldest first. Empty when none. | array(BillOfMaterialsCost) | true |
| description | Free-text description of this bill of materials, or null when none was entered | string | false |
| dynamic_inputs | 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. | array(BillOfMaterialsDynamicInput) | true |
| id | ID for this bill of materials | string | true |
| name | Human readable name for this bill of materials | string | true |
| product_inputs | 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. |
array(BillOfMaterialsProductInput) | true |
BillOfMaterialsCost
An additional cost applied by a bill of materials
| Property | Description | Type | Required |
|---|---|---|---|
| cost_type | The cost type applied by a bill-of-materials cost | BillOfMaterialsCostType | false |
| description | Free-text description of this cost, or null when none was entered | string | false |
| id | ID for this cost | string | true |
| quantity | 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. |
string | true |
BillOfMaterialsCostType
The cost type applied by a bill-of-materials cost
| Property | Description | Type | Required |
|---|---|---|---|
| cost_per_unit | 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. |
string | false |
| id | ID for this cost type | string | true |
| name | Human readable name for this cost type | string | true |
| unit_type | 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. | UnitType | false |
BillOfMaterialsDynamicInput
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.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this input | string | true |
| product_categories | The product categories this input matches on, or empty when it does not match on category. A product must match every non-empty criterion. | array(ProductCategoryCompact) | true |
| product_groups | The product groups this input matches on, or empty when it does not match on group. | array(ProductGroupCompact) | true |
| product_subcategories | The product subcategories this input matches on, or empty when it does not match on subcategory. | array(ProductSubcategoryCompact) | true |
| quantity | How much this recipe consumes of whatever product matches, as a decimal string (e.g. "2.5"). | string | true |
| strains | The strains this input matches on, or empty when it does not match on strain. | array(Strain) | true |
| tags | The tags this input matches on, or empty when it does not match on tag. | array(ProductTagRef) | true |
| unit_type | 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. | UnitType | false |
BillOfMaterialsProductInput
A specific product consumed by a bill of materials
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this input | string | true |
| product | 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). |
Product | false |
| quantity | How much of the product this recipe consumes, in the product's unit, as a decimal string (e.g. "2.5"). | string | true |
Bin
A bin used to track where inventory is physically stored
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this bin | string | true |
| inserted_datetime | When the bin was created (UTC ISO-8601) | string | true |
| name | The name of the bin | string | true |
| updated_datetime | When the bin was last updated (UTC ISO-8601) | string | true |
BinCompact
Minimal details about a bin, as nested on other records
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this bin | string | true |
| name | The name of the bin | string | true |
BinResponse
A single bin
| Property | Description | Type | Required |
|---|---|---|---|
| data | A bin used to track where inventory is physically stored | Bin | false |
Bins
A collection of bins
| Property | Description | Type | Required |
|---|---|---|---|
| data | Bins | array(Bin) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
CancelCredit
Options for canceling a credit
| Property | Description | Type | Required |
|---|---|---|---|
| should_delete_credit_uses | 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. | boolean | false |
Charge
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.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this charge | string | true |
| inserted_datetime | The datetime this charge was created at | string | true |
| name | Human-readable label for this line (e.g. "Delivery Fee"). | string | true |
| percent | 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. |
string | false |
| price | The flat money amount of this line, as a 2-decimal string (e.g. "25.00"). Always present. Negative for a DISCOUNT. | string | true |
| tax.id | ID for this Tax | string | false |
| tax.name | The name of this tax | string | false |
| type | Whether this line adds to or subtracts from the total (SCREAMING_CASE).
DISCOUNT CHARGE |
string | true |
| unit_type | How this line's amount is expressed (SCREAMING_CASE).
PERCENT PRICE |
string | true |
CogsReport
The Cost of Goods Sold report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(CogsReportRow) | true |
| meta | Report-level metadata | CogsReportMeta | true |
CogsReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key used for this column in each data row | string | true |
| label | The human-readable label of the column | string | true |
CogsReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. |
array(CogsReportColumn) | true |
| date_range | 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. | string | true |
| report | The report identifier (always cogs). |
string | true |
CogsReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| batch | 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. |
string | false |
| cost_origin | Where a component's cost came from. Always null on this endpoint, since only the sold-item rows are returned. | string | false |
| final_input | 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. |
string | true |
| margin_actual | 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. |
string | false |
| margin_default | 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. |
string | false |
| metrc_production_batch_number | 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. |
string | false |
| order_number | 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. | string | true |
| package | The package's compliance label, or null when the line item is not package-tracked or the package has no label. |
string | false |
| product_brand | The product's brand name, or null when the product has no brand assigned. |
string | false |
| product_category | The product's category name. Always present — every product has a category. | string | true |
| product_name | The product name. | string | true |
| profit_unit_actual | unit_price minus unit_cost_actual. Null when the actual cost cannot be traced. |
string | false |
| profit_unit_default | unit_price minus unit_cost_default. Null when the default cost cannot be traced. |
string | false |
| quantity | The quantity sold, net of any returned quantity on the line item. | string | true |
| sku | The product SKU. | string | true |
| total_cost_actual | Actual total cost — unit_cost_actual multiplied by the row's quantity. Null when unit_cost_actual is null. |
string | false |
| total_cost_default | Default (standard) total cost — unit_cost_default multiplied by the row's quantity. Null when unit_cost_default is null. |
string | false |
| total_price | The total price (unit price times quantity). | string | true |
| total_profits_actual | Total price minus total_cost_actual. Null when the actual cost cannot be traced. |
string | false |
| total_profits_default | Total price minus total_cost_default. Null when the default cost cannot be traced. |
string | false |
| unit_cost_actual | 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. | string | false |
| unit_cost_default | 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. |
string | false |
| unit_price | The price per unit. | string | true |
| unit_type | The item's unit type name. | string | true |
CompactCredit
A compact representation of a credit
| Property | Description | Type | Required |
|---|---|---|---|
| amount | The current spendable face value of this credit, as a decimal string (e.g. "100.00"). |
string | true |
| credit_number | The credit number as shown in the Distru UI (e.g. CR-1001). |
string | true |
| id | ID for this credit | string | true |
| source | How this credit was created (SCREAMING_CASE): USER, RETURN, INVOICE_PAYMENT, QB_PAYMENT, or QB_CREDIT_MEMO. INVOICE_PAYMENT QB_CREDIT_MEMO QB_PAYMENT RETURN USER |
string | true |
CompactInvoice
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.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this invoice | string | true |
| invoice_number | The invoice number as shown in the Distru UI (e.g. "INV-0001"). Unique per company. | string | true |
| status | 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.
|
string | true |
| total | The invoice total (line items plus charges/taxes minus discounts), as a decimal string (e.g. "150.50"). | string | true |
CompactMenu
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.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID of the menu | string | true |
| name | Display name of the menu | string | true |
CompactOrder
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.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this order | string | true |
| order_number | The order number as shown in the Distru UI (e.g. "ORD-0001"). Unique per company. | string | true |
| status | The fulfillment status of this order (SCREAMING_CASE) — its lifecycle stage, distinct from an invoice's payment status. | string | true |
| total | The order total (line items plus charges/taxes minus discounts), as a 2-decimal string (e.g. "150.50"). | string | true |
CompactOrderItem
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.
| Property | Description | Type | Required |
|---|---|---|---|
| batch | 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. | Batch | false |
| compliance_quantity | 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. | string | false |
| id | ID for this order item | string | true |
| is_sample | True if this order item is a sample given away rather than sold. | boolean | true |
| location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | false |
| package | 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. | Package | false |
| price | 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. |
string | true |
| price_base | The per-unit list price of this order item before any price tier discount, as a decimal string (e.g. "30.00"). | string | true |
| product | 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). |
Product | false |
| quantity | Quantity sold on this order item, expressed in the product's unit type, as a decimal string (e.g. "10") | string | true |
CompactReturn
A compact representation of a return
| Property | Description | Type | Required |
|---|---|---|---|
| company | 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. | CompanyCompact | false |
| id | ID for this return | string | true |
| return_datetime | The date of this return, as YYYY-MM-DD (e.g. 2022-07-10) |
string | false |
| return_number | The return number as shown in the Distru UI (e.g. RET-1001) |
string | false |
| status | The return's status (SCREAMING_CASE): PROCESSING, SHIPPED, RECEIVED, or COMPLETED. PROCESSING SHIPPED RECEIVED COMPLETED |
string | false |
| total | The total value of this return | number | true |
Companies
A collection of companies
| Property | Description | Type | Required |
|---|---|---|---|
| data | Companies | array(Company) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Company
A business in your network — a customer, a vendor, or both. Holds contact details, locations, licenses, and the terms you deal on.
| Property | Description | Type | Required |
|---|---|---|---|
| category | 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. Other Cultivator Delivery Dispensary Distributor Lab Manufacturer Microbusiness Retail |
string | false |
| custom_data | The custom data for this company | array(CustomField) | true |
| default_email | The primary email address for this company, or null when not set | string | false |
| default_payment_term | The agreed timeframe a customer has to pay — for example "Net 30" means payment is due 30 days after the invoice. | PaymentTerm | false |
| default_purchase_order_notes | The default notes automatically added to purchase orders when this company is the supplier, or null when not set | string | false |
| default_sales_order_notes | The default external notes automatically added to sales orders when this company is the customer, or null when not set | string | false |
| deleted_at | ISO 8601 datetime this company was deleted at, or null when it is not deleted | string | false |
| group | A label used to group companies together (for example by territory or account tier) for organizing and reporting. | CompanyGroup | false |
| id | ID for this company | string | true |
| inserted_datetime | ISO 8601 datetime this company was created at | string | false |
| invoice_email | The email address where sales order invoices are delivered, or null when not set | string | false |
| leaflink_brand_id | The LeafLink brand ID mapped to this company; only set on self-relationships (your own company), otherwise null | integer | false |
| leaflink_customer_id | The LeafLink customer ID mapped to this company, or null when it is not mapped to a LeafLink customer | integer | false |
| legal_business_name | The legal business name for this company. Empty string when never set. | string | false |
| licenses | The licenses held by this company. Empty when the company has none. | array(License) | false |
| locations | The locations belonging to this company. Empty when the company has none. | array(LocationCompact) | false |
| name | Human readable name for this company | string | true |
| order_shipment_email | The email address where sales order shipment packing slips are delivered, or null when not set | string | false |
| outstanding_balance | 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). | string | false |
| outstanding_balance_threshold | 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. |
integer | false |
| owner | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| owner_id | The ID of the Distru user who is the account owner (main point of contact) for this company, or null when no owner is assigned | string | false |
| phone_number | The phone number for this company, or null when not set | string | false |
| purchase_order_email | The email address where purchase order slips are delivered, or null when not set | string | false |
| qb_customer_id | The QuickBooks Online customer ID mapped to this company, or null when it is not mapped to a QuickBooks Online customer | string | false |
| qb_vendor_id | The QuickBooks Online vendor ID mapped to this company, or null when it is not mapped to a QuickBooks Online vendor | string | false |
| relationship_type | How a company relates to your business — whether they are a customer you sell to, a vendor you buy from, or both. | RelationshipType | false |
| sales_order_email | The email address where sales order slips are delivered, or null when not set | string | false |
| updated_datetime | 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. | string | true |
| website | The website for this company, or null when not set | string | false |
CompanyCompact
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.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this company | string | true |
| name | Human readable name for this company | string | true |
| updated_datetime | ISO 8601 datetime this company was last updated at | string | true |
CompanyGroup
A label used to group companies together (for example by territory or account tier) for organizing and reporting.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this company group | string | true |
| name | Name of the company group | string | true |
CompanyGroupFull
A company group
| Property | Description | Type | Required |
|---|---|---|---|
| id | 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. |
string | true |
| inserted_datetime | When the company group was created, as a UTC ISO-8601 timestamp (e.g. "2026-08-20T14:30:00Z"). Always present. |
string | true |
| name | Display name of the company group. Always present, unique within your company. | string | true |
| updated_datetime | 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. |
string | true |
CompanyGroupFullResponse
A single company group
| Property | Description | Type | Required |
|---|---|---|---|
| data | A company group | CompanyGroupFull | false |
CompanyGroups
A collection of company groups
| Property | Description | Type | Required |
|---|---|---|---|
| data | Company Groups | array(CompanyGroupFull) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
CompanyResponse
A single company relationship
| Property | Description | Type | Required |
|---|---|---|---|
| data | A business in your network — a customer, a vendor, or both. Holds contact details, locations, licenses, and the terms you deal on. | Company | false |
Contact
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.
| Property | Description | Type | Required |
|---|---|---|---|
| company.id | The ID of the company this contact belongs to | string | false |
| custom_data | The custom data for this contact | array(CustomField) | true |
| deleted_at | ISO 8601 datetime this contact was deleted at, or null when it is not deleted | string | false |
| description | Free-text description of this contact, or null when none was entered | string | false |
| driver_license_issuing_state | The state that issued the driver license used on shipping manifests, or null when not set | string | false |
| driver_license_number | Driver license number used on shipping manifests, or null when not set | string | false |
| The email address of this contact, or null when not set | string | false | |
| first_name | The first name of this contact, or null when not set | string | false |
| full_name | The full name of this contact, or null when not set | string | false |
| id | ID for this contact | string | true |
| inserted_datetime | ISO 8601 datetime this contact was created at | string | true |
| last_name | The last name of this contact, or null when not set | string | false |
| owner | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| phone_number | The phone number of this contact, or null when not set | string | false |
| title | The job title of this contact, or null when not set | string | false |
| updated_datetime | ISO 8601 datetime this contact was last updated at | string | true |
| work_phone_number | The work phone number of this contact, or null when not set | string | false |
ContactResponse
A single contact
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | Contact | false |
Contacts
A collection of Contacts
| Property | Description | Type | Required |
|---|---|---|---|
| data | Contacts | array(Contact) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
CostEntryInput
A single cost to apply to a record
| Property | Description | Type | Required |
|---|---|---|---|
| cost_per_unit | 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 | number | false |
| cost_type_id | 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 | string | true |
| description | Optional free-form text stored on the cost | string | false |
| quantity | 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 | number | true |
CostType
A cost type
| Property | Description | Type | Required |
|---|---|---|---|
| active | Whether the cost type is active and selectable when applying new costs. Always present; inactive cost types are still returned by the read endpoints. | boolean | true |
| allow_inline_edits | 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. |
boolean | true |
| cost_per_unit | 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. |
string | true |
| deleted_at | 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. | string | false |
| description | Free-text description of the cost type, or null if none was set. | string | false |
| id | ID of this cost type. Stable across renames; use it as the id for the fetch, upsert, and delete endpoints. |
string | true |
| inserted_datetime | When the cost type was created, as a UTC ISO-8601 timestamp (e.g. "2026-08-20T14:30:00Z"). Always present. |
string | true |
| name | Display name of the cost type. Always present and unique within your company (case-insensitive) among active, non-deleted cost types. | string | true |
| unit_type | 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. | UnitType | true |
| updated_datetime | 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. |
string | true |
CostTypeResponse
A single cost type
| Property | Description | Type | Required |
|---|---|---|---|
| data | A cost type | CostType | false |
CostTypes
A collection of cost types
| Property | Description | Type | Required |
|---|---|---|---|
| data | Cost Types | array(CostType) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Credit
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).
| Property | Description | Type | Required |
|---|---|---|---|
| amount | 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. |
string | true |
| canceled_datetime | The ISO8601 UTC datetime at which the credit was canceled (e.g. 2022-07-10T00:00:00Z). Only set for CANCELED credits; null otherwise. |
string | false |
| company | 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. | CompanyCompact | false |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| credit_number | The human-readable credit number as shown in the Distru UI (e.g. CR-1001). Assigned automatically on create. |
string | true |
| credit_uses | 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. | array(CreditUse) | true |
| deleted_in_qbo | Whether this credit was pushed to QuickBooks Online and later deleted there. False for credits never synced to QuickBooks Online. | boolean | true |
| external_note | A note on this credit, visible to the customer. Null if unset. | string | false |
| id | ID for this credit | string | true |
| inserted_datetime | The ISO8601 UTC datetime at which the credit was created in Distru (e.g. 2022-07-10T00:00:00Z). |
string | true |
| internal_note | An internal note on this credit, not shown to the customer. Null if unset. | string | false |
| original_amount | 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. |
string | true |
| owner | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| payment | A record of money exchanged — received from a customer against an invoice, or paid to a vendor against a purchase order. | Payment | false |
| qb_credit_memo_id | 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). |
string | false |
| qb_payment_id | 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. |
string | false |
| qb_sync_status | 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). PENDING ERROR DELETED_IN_QBO NOT_SYNCED PARTIALLY_SYNCED SYNCED |
string | false |
| remaining_balance | 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). |
string | false |
| return | A compact representation of a return | CompactReturn | false |
| source | 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. INVOICE_PAYMENT QB_CREDIT_MEMO QB_PAYMENT RETURN USER |
string | true |
| status | The credit's live computed status (SCREAMING_CASE), derived from its balance and cancellation state rather than stored.
ACTIVE CANCELED REDEEMED |
string | true |
| updated_datetime | The ISO8601 UTC datetime at which the credit was last updated in Distru. | string | true |
CreditResponse
A single credit wrapped in a data envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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). | Credit | false |
CreditUse
An application of a credit to an invoice payment
| Property | Description | Type | Required |
|---|---|---|---|
| amount | The amount of the credit applied to this invoice payment, as a decimal string (e.g. "25.00"). Always greater than 0. |
string | true |
| credit | A compact representation of a credit | CompactCredit | false |
| id | ID for this credit use | string | true |
| inserted_datetime | The ISO8601 UTC datetime at which this credit use was created in Distru. | string | true |
| payment | A record of money exchanged — received from a customer against an invoice, or paid to a vendor against a purchase order. | Payment | false |
Credits
A collection of Credits
| Property | Description | Type | Required |
|---|---|---|---|
| data | Credits | array(Credit) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
CultivationTransactionHistoryReport
The Cultivation Transaction History report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows, one per cultivation transaction, sorted most-recent-first by transaction date. Empty when no transactions match the filters. | array(CultivationTransactionHistoryReportRow) | true |
| meta | Report-level metadata | CultivationTransactionHistoryReportMeta | true |
CultivationTransactionHistoryReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The machine key for this column; identical to the corresponding property name in each data row (e.g. batch_name, total_cost). |
string | true |
| label | The human-readable column header (e.g. Batch Name, Total Cost). |
string | true |
CultivationTransactionHistoryReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. |
array(CultivationTransactionHistoryReportColumn) | true |
| date_range | 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. |
string | true |
| report | The report identifier, always cultivation_transaction_history. |
string | true |
CultivationTransactionHistoryReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| amount | 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. |
string | false |
| batch_name | The plant batch (plant group) name, or null for rows not tied to a specific batch. | string | false |
| date | The transaction date, formatted MM/DD/YYYY in the company's timezone (not an ISO8601 timestamp). Always present. |
string | true |
| description | The transaction's free-text description, or null when none was recorded. | string | false |
| package_label_s | Comma-separated Metrc package compliance label(s) tied to the transaction, or null when no package is involved. | string | false |
| plant_tag_s | 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. | string | false |
| product_name | The product name associated with the transaction, or null when none applies. | string | false |
| related_entity | 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. | string | false |
| related_entity_status | 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. |
string | false |
| strain | The strain name of the plant batch, or null for rows not tied to a strain (e.g. some harvest- or teardown-level rows). | string | false |
| total_cost | 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. |
string | false |
| type | 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. |
string | true |
| unit | 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. |
string | false |
CustomField
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.
| Property | Description | Type | Required |
|---|---|---|---|
| id | The ID of this custom field's definition (matches an id from GET /public/v1/custom-fields). An integer. |
integer | true |
| name | The name of this custom field | string | true |
| value | This record's value for the custom field, as a string, or null when no value has been set for this record | string | false |
CustomFieldDefinition
A custom field definition
| Property | Description | Type | Required |
|---|---|---|---|
| description | Free-text note describing the field, or null when none was set. | string | false |
| disabled_field_options | 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. |
array(any) | true |
| field_options | 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. |
array(any) | false |
| field_type | 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. |
string | true |
| filterable | Whether records of this entity type can be filtered by this field's value. Always false for text and date fields. Always present. |
boolean | true |
| id | 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. |
integer | true |
| name | Display name of the field. Unique (case-insensitively) among the fields on the same parent_object for the company. Always present. |
string | true |
| parent_object | 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. |
string | false |
| required | Whether a value must be supplied when a record of this entity type is saved in the Distru app. Always present. | boolean | true |
CustomFieldDefinitionResponse
A single custom field definition wrapped in a data envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | A custom field definition | CustomFieldDefinition | false |
CustomFieldDefinitions
A collection of custom field definitions
| Property | Description | Type | Required |
|---|---|---|---|
| data | CustomFieldDefinitions | array(CustomFieldDefinition) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Driver
A driver
| Property | Description | Type | Required |
|---|---|---|---|
| birth_date | 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. |
string | false |
| driver_license | The driver's license number. Populated for both METRC and BIOTRACK drivers, but may be null on older records. | string | false |
| The driver's email. Populated for BIOTRACK drivers; null for METRC drivers. | string | false | |
| first_name | The driver's first name. Always present. | string | true |
| hire_date | 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. |
string | false |
| id | ID of this driver. Use it to fetch, update, or delete the driver. | string | true |
| inserted_datetime | When the driver was created, as a UTC ISO-8601 timestamp. Always present. | string | true |
| last_name | The driver's last name. Always present. | string | true |
| occupational_license_number | The driver's occupational license number. Populated for METRC drivers; null for BIOTRACK drivers. | string | false |
| phone_number | The driver's phone number. Populated for METRC drivers; null for BIOTRACK drivers. | string | false |
| updated_datetime | When the driver was last updated, as a UTC ISO-8601 timestamp. Always present. | string | true |
| us_state | The US state that issued the driver's license (e.g. CA). Populated for BIOTRACK drivers; null for METRC drivers. |
string | false |
DriverResponse
A single driver
| Property | Description | Type | Required |
|---|---|---|---|
| data | A driver | Driver | false |
Drivers
A collection of drivers
| Property | Description | Type | Required |
|---|---|---|---|
| data | Drivers | array(Driver) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Error
A single error describing one problem with the request.
| Property | Description | Type | Required |
|---|---|---|---|
| message | Human-readable, customer-facing description of what went wrong. | string | true |
| pointer | 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. | array(any) | true |
ErrorResponse
The envelope returned by every failed request (400, 401, 403, 404, 429). errors always holds at least one entry.
| Property | Description | Type | Required |
|---|---|---|---|
| errors | One or more errors describing why the request failed. | array(Error) | true |
FileAttachment
A file attachment
| Property | Description | Type | Required |
|---|---|---|---|
| assembly_id | 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. | string | false |
| batch_id | 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. | string | false |
| company_relationship_id | 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. | string | false |
| contact_id | 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. | string | false |
| id | ID of this file attachment. Always present. | string | true |
| invoice_id | 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. | string | false |
| license_id | 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. | string | false |
| mime_type | 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. |
string | false |
| name | Display name of the attachment — the name you sent, or the uploaded file's original filename when none was given. Always present. | string | true |
| order_id | 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. | string | false |
| order_shipment_id | 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. | string | false |
| product_id | 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. | string | false |
| purchase_id | 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. | string | false |
| request_id | 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. | string | false |
| return_id | 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. | string | false |
| size_in_bytes | 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. | integer | false |
| stock_transfer_id | 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. | string | false |
| task_id | 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. | string | false |
| upload_datetime | When the attachment was created, as a UTC ISO-8601 timestamp (e.g. 2026-08-20T14:30:00Z). Always present. |
string | true |
| uploader.id | string | false | |
| uploader.name | string | false | |
| url | Download URL for the file, ready to fetch directly. Populated whenever the file record exists; null only when the underlying file record is missing. | string | false |
FileAttachmentResponse
A single file attachment wrapped in a data envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | A file attachment | FileAttachment | false |
FinishPackagesRequest
| Property | Description | Type | Required |
|---|---|---|---|
| finished_datetime | 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. |
string | false |
| package_ids | 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. |
array(any) | true |
HarvestOutputsReport
The Harvest Outputs report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(HarvestOutputsReportRow) | true |
| meta | Report-level metadata | HarvestOutputsReportMeta | true |
HarvestOutputsReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key used for this column in each data row | string | true |
| label | The human-readable label of the column | string | true |
HarvestOutputsReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. |
array(HarvestOutputsReportColumn) | true |
| date_range | 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. |
string | true |
| report | The report identifier — always harvest_outputs. |
string | true |
HarvestOutputsReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| cost_input_output | 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. |
string | true |
| cost_type | 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. |
string | false |
| cost_type_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. |
string | false |
| distru_product | The Distru product name — the harvest name on Input rows, the output's product on Output rows. Null on Cost rows. |
string | false |
| harvest_assembly_date | The assembly's creation date, formatted MM/DD/YYYY in the requesting user's timezone (e.g. 01/15/2026). Always present. |
string | true |
| harvest_assembly_number | The assembly's human-readable number. Always present. | string | true |
| harvest_name | 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. | string | false |
| line_item_id | Identifies the specific line item this row represents (the input, output, or cost line item, matching cost_input_output). Always present. |
string | true |
| location | 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. |
string | false |
| output_batch_number | The output's batch number. Populated only on Output rows; null on Input and Cost rows. |
string | false |
| output_package_number | 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. |
string | false |
| output_reference_id | The ID of the output this cost line item belongs to. Populated only on Cost rows; null on Input and Output rows. |
string | false |
| product_category | The output product's category. Populated only on Output rows; null on Input and Cost rows. |
string | false |
| quantity | 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. |
string | true |
| status | The assembly status: PENDING (not yet completed) or COMPLETED. Same value on every row of the assembly. SCREAMING_CASE. Always present.PENDING COMPLETED |
string | true |
| strain | The strain name (the harvest's Metrc strain). Sourced like harvest_name. Null when the harvest has no strain recorded. |
string | false |
| total_cost_actual | 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. |
string | false |
| total_cost_default | 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. |
string | false |
| unit_cost_actual | 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. | string | false |
| unit_cost_default | 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. |
string | false |
| unit_type | 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. |
string | false |
Image
An image as shown in Distru
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this image | string | true |
| name | Original file name of this image, or null when unknown | string | false |
| rank | Sort position of this image among the product's images (lower ranks shown first), or null when unranked | integer | false |
| url | URL to the full-size (original) image file, or null when no file is available | string | false |
Inventories
A list of active and available quantity for each group
| Property | Description | Type | Required |
|---|---|---|---|
| data | Inventories | array(Inventory) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Inventory
| Property | Description | Type | Required |
|---|---|---|---|
| active | Total on-hand quantity for this group, as a decimal string (e.g. "10.000000000"). Always present. | string | true |
| available | 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. |
string | true |
| batch_number | 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. |
string | false |
| cost_per_unit_actual | 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. |
string | false |
| cost_per_unit_default | 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. |
string | false |
| location_id | 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). |
string | false |
| product_id | Distru product id this group belongs to. Always present. | string | true |
| reserved | 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. |
string | true |
| total_cost_actual | 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. | string | false |
| total_cost_default | 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. |
string | false |
| updated_datetime | ISO8601 UTC timestamp of the most recent change to any stock counted in this group. Always present. | string | true |
InventoryAssetsReport
The Inventory Assets report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(InventoryAssetsReportRow) | true |
| meta | Report-level metadata | InventoryAssetsReportMeta | true |
InventoryAssetsReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | Key under which this column's value appears on every row in data. |
string | true |
| label | Human-readable column label as shown in the CSV export. | string | true |
InventoryAssetsReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. |
array(InventoryAssetsReportColumn) | true |
| date_range | 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. |
string | true |
| report | Report identifier; always inventory_assets. |
string | true |
InventoryAssetsReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| active_quantity | 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. |
string | false |
| assembling_quantity | Quantity claimed by in-progress assemblies as of the snapshot. Null on Input rows. |
string | false |
| batch_number | 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. |
string | false |
| category | Product's category name. | string | false |
| cost_origin | 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. |
string | false |
| cost_quantity | Present only in granular style. Quantity of this cost input attributed to the asset. Null on Final rows; absent in collapsed style. |
string | false |
| expiration_date | 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. |
string | false |
| final_input | 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. |
string | false |
| harvest_date | Harvest date recorded on the asset's package. Null for assets that are not package-tracked, and null on Input rows. |
string | false |
| license | License number of the asset's location. Null when the location has no license, and null on Input rows. |
string | false |
| location | Location name. Null on Input rows. |
string | false |
| owner | Full name of the product's owner. Null when the product has no owner. | string | false |
| package_number | Compliance label of the asset's package. Null for assets that are not package-tracked. | string | false |
| product | 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. |
string | false |
| selling_quantity | Quantity committed to open sales orders as of the snapshot. Null on Input rows. |
string | false |
| sku | Product SKU, or an empty string when the product has no SKU. | string | false |
| subcategory | Product's subcategory name. Null when the product has none. | string | false |
| total_cost_actual | 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. |
string | false |
| total_cost_default | 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. |
string | false |
| tracking_method | How the product's inventory is tracked: PACKAGE, BATCH, or PRODUCT. Null on Input rows whose cost component is not tied to a product.PACKAGE BATCH PRODUCT |
string | false |
| unit_cost_actual | 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. | string | false |
| unit_cost_default | 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. |
string | false |
| unit_price | Product's configured unit price. Null on Input rows. |
string | false |
| unit_type | Unit-of-measure name for the row's quantities. | string | false |
| vendor | Product's vendor name. | string | false |
InventoryTransactionHistoryReport
The Inventory Transaction History report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(InventoryTransactionHistoryReportRow) | true |
| meta | Report-level metadata | InventoryTransactionHistoryReportMeta | true |
InventoryTransactionHistoryReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key used for this column in each data row | string | true |
| label | The human-readable label of the column | string | true |
InventoryTransactionHistoryReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. |
array(InventoryTransactionHistoryReportColumn) | true |
| date_range | The human-readable date range the report covers, reflecting the resolved datetime filter (or the default last-30-days window when it was omitted). |
string | true |
| report | The report identifier, always inventory_transaction_history. |
string | true |
InventoryTransactionHistoryReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| amount | The signed transaction quantity, measured in unit_type: negative when stock left inventory, positive when it entered. |
string | true |
| batch_id | 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. |
string | false |
| batch_number | The Distru batch number of the transaction's batch. Null unless the product is batch-tracked. | string | false |
| cbd | The package's CBD potency, as a percentage. Same population rule as thc. |
string | false |
| cbd_mg_g | 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. |
string | false |
| cbd_mg_ml | 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. |
string | false |
| company_relationship_id | 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). | string | false |
| date | The transaction's date and time, formatted YYYY-MM-DD HH:MM in the company's timezone (e.g. 2026-01-15 14:30). |
string | true |
| description | The transaction's description. | string | true |
| metrc_production_batch_number | 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. | string | false |
| metrc_unit_name | 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. | string | false |
| package_batch_number_or_batch_name | 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). | string | false |
| package_label | The package's compliance label. Null when the transaction is not tied to a package. | string | false |
| product | The product's name. When the product has been deactivated the name is prefixed to mark it inactive. | string | true |
| product_id | The Distru product ID of the transaction's product. Accepts the same value the product_ids filter takes. |
string | false |
| related_entity | 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. |
string | false |
| related_entity_customer_vendor | 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). | string | false |
| related_entity_status | 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. |
string | false |
| thc | 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). | string | false |
| thc_mg_g | 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. |
string | false |
| thc_mg_ml | 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. |
string | false |
| total_cbd | The package's total CBD (post-decarboxylation), as a percentage. Same population rule as thc. |
string | false |
| total_cbd_mg_g | The package's total CBD in mg per gram. Populated only when the primary test result reports potency in mg/g; null otherwise. | string | false |
| total_cbd_mg_ml | The package's total CBD in mg per millilitre. Populated only when the primary test result reports potency in mg/mL; null otherwise. | string | false |
| total_cost | 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. | string | false |
| total_thc | The package's total THC (post-decarboxylation), as a percentage. Same population rule as thc. |
string | false |
| total_thc_mg_g | The package's total THC in mg per gram. Populated only when the primary test result reports potency in mg/g; null otherwise. | string | false |
| total_thc_mg_ml | The package's total THC in mg per millilitre. Populated only when the primary test result reports potency in mg/mL; null otherwise. | string | false |
| type | 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. |
string | false |
| unit_type | The name of the unit amount is measured in — the package's unit for package transactions, otherwise the product's unit. |
string | true |
InventoryValuationReport
The Inventory Valuation report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(InventoryValuationReportRow) | true |
| meta | Report-level metadata | InventoryValuationReportMeta | true |
InventoryValuationReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key used for this column in each data row | string | true |
| label | The human-readable label of the column | string | true |
InventoryValuationReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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). |
array(InventoryValuationReportColumn) | true |
| date_range | 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. | string | true |
| report | The report identifier (always inventory_valuation) |
string | true |
InventoryValuationReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| active_quantity | The active on-hand quantity, counting only active inventory in the requested locations/users. Defaults to 0 when the product has no active stock. | string | true |
| active_value_price | 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). |
string | false |
| assembling_quantity | The quantity currently reserved inside in-progress assemblies. 0 when none. | string | true |
| available_quantity | active_quantity minus reserved_quantity. Can be negative when reservations exceed on-hand stock. |
string | true |
| brand | The name of the product's brand, or null when it has none | string | false |
| category | The product's category, or null when it has none | string | false |
| group | The product's group, or null when it has none | string | false |
| image_url | URL of the product's thumbnail image, or null when it has no image | string | false |
| incoming_quantity | The quantity on open (not-yet-received) purchases. 0 when none. | string | true |
| inventory_threshold_max | The product's inventory alert maximum, or null when no threshold is set | string | false |
| inventory_threshold_min | The product's inventory alert minimum, or null when no threshold is set | string | false |
| name | The product name. Inactive products are prefixed to mark them. | string | true |
| owner | The full name of the product's owner, or null when unassigned | string | false |
| pending_output_quantity | The output quantity still owed by open assemblies. 0 when none. | string | true |
| reserved_quantity | The quantity reserved against open sales orders. 0 when none. | string | true |
| sku | The product SKU | string | true |
| subcategory | The product's subcategory, or null when it has none | string | false |
| unit_cost | The product's unit cost, or null when no cost is set. Used for the value column when calculation_method=cost. |
string | false |
| unit_price | The product's unit price. Used for the value column by default (calculation_method=price). |
string | true |
| unit_type | The product's unit of measure (e.g. Each, Gram) |
string | true |
| vendor | The name of the product's vendor | string | true |
Invoice
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.
| Property | Description | Type | Required |
|---|---|---|---|
| billing_location | A location with its license number inlined, as nested on orders/invoices/purchases | LocationWithLicense | false |
| charges | A collection of Charges | array(Charge) | true |
| company | 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. | CompanyCompact | false |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| custom_data | A collection of CustomData | array(CustomField) | true |
| due_datetime | The datetime by which the customer should pay the invoice. | string | true |
| external_notes | Notes on this invoice that are visible to the customer, or null. | string | false |
| id | ID for this invoice. | string | true |
| inserted_datetime | The datetime at which the invoice was created in Distru. | string | true |
| internal_notes | Notes on this invoice that are only visible internally, or null. | string | false |
| invoice_datetime | The datetime the invoice is dated for. | string | true |
| invoice_number | 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. |
string | true |
| items | A collection of InvoiceItems | array(InvoiceItem) | true |
| order | 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. | CompactOrder | false |
| owner | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| paid_amount | 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. |
string | true |
| payment_term_name | The name of the payment term applied to this invoice (e.g. "Net 30"), or null when no payment term is set. | string | false |
| payments | A collection of the invoice's payments | array(Payment) | true |
| remaining_amount | The outstanding balance, as a decimal string: the invoice total minus paid_amount. Negative when the invoice is over-paid. |
string | true |
| status | 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. NOT_PAID PARTIALLY_PAID FULLY_PAID OVER_PAID |
string | true |
| total | The invoice total as a decimal string, including all line items, taxes, and discounts. Recomputed by Distru on every save. | string | true |
| updated_datetime | The datetime at which the invoice was last updated in Distru. | string | true |
| voided_datetime | 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. | string | false |
InvoiceChargeRequest
Invoice charge params
| Property | Description | Type | Required |
|---|---|---|---|
| id | 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. |
string | false |
| name | The label for this charge line, shown on the invoice. | string | false |
| percent | 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. |
number | false |
| price | 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. |
number | false |
| type | 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). CHARGE DISCOUNT |
string | true |
| unit_type | 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).PERCENT PRICE |
string | true |
InvoiceHistoryReport
The Invoice History report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(InvoiceHistoryReportRow) | true |
| meta | Report-level metadata | InvoiceHistoryReportMeta | true |
InvoiceHistoryReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key this column uses in each data row |
string | true |
| label | The human-readable label of the column | string | true |
InvoiceHistoryReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. |
array(InvoiceHistoryReportColumn) | true |
| date_range | 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. |
string | true |
| report | The report identifier, always invoice_history |
string | true |
InvoiceHistoryReportRow
A single row of the Invoice History report. Companies on a compliance integration and companies with Invoice custom fields will see additional keys.
| Property | Description | Type | Required |
|---|---|---|---|
| charge_summary | Human-readable per-charge breakdown of the positive charges. Null when the invoice has none. | string | false |
| customer | The customer's company name | string | true |
| discount_summary | Human-readable per-discount breakdown. Null when the invoice has no discounts. | string | false |
| due_date | The invoice due date as YYYY-MM-DD in the API key user's timezone. Null when the invoice has no due date set. |
string | false |
| invoice_date | The invoice date as YYYY-MM-DD in the API key user's timezone. Null when the invoice has no invoice date set. |
string | false |
| invoice_number | The invoice number. Null when the invoice has none. | string | false |
| line_item_subtotal | Sum of the invoice's line items (quantity times unit price); 0 when there are none. |
string | true |
| outstanding | The unpaid balance: invoice total minus amount paid. Negative when overpaid. | string | true |
| owner | Full name of the invoice's owner (the assigned user). Null when the invoice has no owner. | string | false |
| paid | The total amount paid on the invoice across its non-voided payments; 0 when nothing has been paid. |
string | true |
| sales_order | The order number of the sales order this invoice belongs to | string | true |
| status | 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. NOT_PAID PARTIALLY_PAID FULLY_PAID OVER_PAID |
string | false |
| tax_summary | Human-readable per-tax breakdown, e.g. Excise Tax - $12.50, City Tax - $4.00. Null when the invoice has no taxes. |
string | false |
| total | The invoice grand total | string | true |
| total_charges | Sum of the invoice's positive (non-discount, non-tax) charges; 0 when none. |
string | true |
| total_discounts | Sum of the invoice's discounts (negative charges), expressed as a negative number; 0 when none. |
string | true |
| total_taxes | Sum of the invoice's taxes; 0 when none. |
string | true |
InvoiceItem
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.
| Property | Description | Type | Required |
|---|---|---|---|
| description | A free-text description for this billed line, or null when none was set. | string | false |
| id | ID for this invoice item. | string | true |
| inserted_datetime | The datetime this invoice item was created. | string | true |
| order_item | A single product line on a sales order — what is being sold, how much, at what price, and which inventory (batch/package) fulfills it. | SalesOrderItem | false |
| order_item_id | 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. |
string | true |
| quantity | 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. | string | false |
InvoiceItemRequest
Invoice item params
| Property | Description | Type | Required |
|---|---|---|---|
| description | An optional free-text description for this billed line. | string | false |
| id | 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. |
string | false |
| order_item_id | 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. |
string | true |
| quantity | 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. |
number | true |
InvoiceResponse
A single invoice wrapped in a data envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | Invoice | false |
Invoices
A collection of Invoices
| Property | Description | Type | Required |
|---|---|---|---|
| data | Invoices | array(Invoice) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
License
A cannabis license held by a company or tied to a location, identifying it to the state and its compliance system.
| Property | Description | Type | Required |
|---|---|---|---|
| active | Whether this license is currently active | boolean | true |
| expiry_datetime | ISO 8601 datetime this license expires (e.g. "2026-08-20T00:00:00Z") | string | true |
| id | ID for this license | string | true |
| inserted_datetime | ISO 8601 datetime this license was created at | string | true |
| issue_datetime | ISO 8601 datetime this license was issued, or null when not set | string | false |
| license_number | License number | string | true |
| license_type | The license type as configured in Distru. A state-specific free-form value, e.g. "Distributor" or "Type 11 Distributor-Transport" | string | true |
Location
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.
| Property | Description | Type | Required |
|---|---|---|---|
| address | The full address as a single formatted line, built from the street, apt, city, state, zip, and country fields | string | true |
| apt | The apartment/suite/unit of this location, or null when none was entered | string | false |
| city | The city of this location | string | true |
| company_id | ID of the company that owns this location | string | false |
| country | The country of this location | string | true |
| deleted_at | ISO 8601 datetime this location was deleted at, or null when it is not deleted | string | false |
| id | ID for this location | string | true |
| inserted_datetime | ISO 8601 datetime this location was created at | string | true |
| latitude | The latitude of this location, or null when it has not been geocoded | number | false |
| license | A cannabis license held by a company or tied to a location, identifying it to the state and its compliance system. | License | false |
| license_id | ID of the license this location is associated with, or null when the location has no license | string | false |
| longitude | The longitude of this location, or null when it has not been geocoded | number | false |
| metrc_id | The location's ID in Metrc. A Metrc identifier, not a Distru ID. Null when the location is not linked to Metrc. | integer | false |
| name | Human readable name for this location | string | true |
| state | The state of this location | string | true |
| street_address | The street address of this location | string | true |
| updated_datetime | ISO 8601 datetime this location was last updated at | string | true |
| zip | The postal code of this location | string | true |
LocationCompact
A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint.
| Property | Description | Type | Required |
|---|---|---|---|
| address | The full address as a single formatted line, built from the street, apt, city, state, zip, and country fields | string | true |
| company_id | ID of the company that owns this location | string | false |
| id | ID for this location | string | true |
| license_id | ID of the license this location is associated with, or null when the location has no license | string | false |
| name | Human readable name for this location | string | true |
LocationResponse
A single location wrapped in a data envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | Location | false |
LocationWithLicense
A location with its license number inlined, as nested on orders/invoices/purchases
| Property | Description | Type | Required |
|---|---|---|---|
| address | The full address as a single formatted line, built from the street, apt, city, state, zip, and country fields | string | true |
| company_id | ID of the company that owns this location | string | false |
| id | ID for this location | string | true |
| license_id | ID of the license this location is associated with, or null when the location has no license | string | false |
| license_number | The license number of the location's license, inlined so you need not fetch it; null when the location has no license | string | false |
| name | Human readable name for this location | string | true |
Locations
A collection of Locations
| Property | Description | Type | Required |
|---|---|---|---|
| data | Locations | array(Location) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Menu
| Property | Description | Type | Required |
|---|---|---|---|
| active | Whether the menu is active. Inactive menus stay configured but are not served to customers. Always present. | boolean | true |
| available_delivery_days | 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. | array(any) | true |
| default_order_status | Status assigned to every order a customer places through this menu. One of PENDING or PROCESSING. Always present. PENDING PROCESSING |
string | true |
| discoverable | Whether the menu is listed on the DistruCommerce marketplace. Can only be true when visibility is PUBLIC. Always present. |
boolean | true |
| external_name | The customer-facing menu name shown to people viewing the menu. Always present (may be an empty string if never set). | string | true |
| id | The menu's ID. Use it as the {id} path segment of the show endpoint. |
string | true |
| inserted_datetime | When the menu was created, as a UTC ISO-8601 timestamp. Always present. | string | true |
| internal_name | 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. | string | true |
| minimum_order_lead_time_days | 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. | integer | true |
| minimum_order_subtotal | 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. |
string | false |
| product_count | Number of active products currently on the menu; inactive and deleted products are excluded. Always present (0 when the menu has no active products). | integer | true |
| updated_datetime | When the menu was last updated, as a UTC ISO-8601 timestamp. Always present. | string | true |
| url | The menu's primary public URL; null when the menu has no primary URL configured. | string | false |
| visibility | 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). PUBLIC PRIVATE PASSCODE_PROTECTED |
string | true |
MenuResponse
A single menu wrapped in a data envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | Menu | false |
Menus
A collection of menus
| Property | Description | Type | Required |
|---|---|---|---|
| data | The menus on this page, ordered oldest-first by creation time. An empty array when the company has no matching menus. | array(Menu) | true |
| next_page | URL for the next page of results; null when this is the last page. | string | false |
MetrcItem
A Metrc item: an item definition synced from Metrc, scoped to one of your licenses.
| Property | Description | Type | Required |
|---|---|---|---|
| inserted_datetime | The datetime this item was first cached in Distru (not a Metrc timestamp) | string | true |
| is_deleted | True when the item has been deleted in Metrc. | boolean | true |
| license | A cannabis license held by a company or tied to a location, identifying it to the state and its compliance system. | License | false |
| metrc_id | The item's identifier in Metrc. A Metrc identifier, not a Distru ID. | integer | true |
| metrc_inserted_datetime | ISO 8601 datetime this item was created in Metrc, or null when Metrc reports none | string | false |
| metrc_strain_id | The item's strain identifier in Metrc. A Metrc identifier, not a Distru ID. Null when the item has no strain. | integer | false |
| metrc_unit_name | The Metrc unit-of-measure name for this item (e.g. "Grams"), or null when none is reported | string | false |
| name | The item name as reported by Metrc, or null when Metrc reports none | string | false |
| product_category_name | The Metrc product category name, or null when none is reported | string | false |
| product_category_type | The Metrc product category type, or null when none is reported | string | false |
| quantity_type | How the item's quantity is measured. Null when Metrc reports none. COUNT_BASED VOLUME_BASED WEIGHT_BASED |
string | false |
| strain_name | The strain name as reported by Metrc, or null when the item has no strain | string | false |
| unit_type | 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. | UnitType | false |
| updated_datetime | The datetime this item's cache was last updated in Distru (not a Metrc timestamp) | string | true |
MetrcItems
A collection of Metrc items. Note: This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.
| Property | Description | Type | Required |
|---|---|---|---|
| data | Metrc Items | array(MetrcItem) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
MetrcTag
A Metrc tag: a unique compliance identifier provisioned to a license for tracking a package or plant in the state cannabis system.
| Property | Description | Type | Required |
|---|---|---|---|
| assigned_datetime | ISO 8601 datetime this tag was assigned to a package or plant, or null while it is still unassigned | string | false |
| commissioned_date | The ISO 8601 date this tag was commissioned in Metrc (e.g. "2026-08-20"), or null when not yet commissioned | string | false |
| id | Distru's ID for this Metrc tag (not a Metrc identifier) | string | true |
| inserted_datetime | The datetime this tag was created in Distru (not a Metrc timestamp) | string | true |
| is_assigned | True once this tag has been assigned to a package or plant, false while still available. Mirrors whether assigned_datetime is set. |
boolean | true |
| kind | Whether the tag is for a package or a plant PACKAGE PLANT |
string | true |
| license_id | ID of the license this tag is provisioned to | string | true |
| tag | The Metrc tag label | string | true |
| updated_datetime | The datetime this tag was last updated in Distru (not a Metrc timestamp) | string | true |
MetrcTagResponse
A single Metrc tag
| Property | Description | Type | Required |
|---|---|---|---|
| data | A Metrc tag: a unique compliance identifier provisioned to a license for tracking a package or plant in the state cannabis system. | MetrcTag | false |
MetrcTags
A collection of Metrc tags. Note: This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.
| Property | Description | Type | Required |
|---|---|---|---|
| data | Metrc Tags | array(MetrcTag) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
MovePackagesRequest
| Property | Description | Type | Required |
|---|---|---|---|
| location_id | 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. |
string | true |
| metrc_location_id | 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. | string | false |
| package_ids | 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. |
array(any) | true |
OfficialProductCategories
A collection of official product categories
| Property | Description | Type | Required |
|---|---|---|---|
| data | OfficialProductCategories | array(OfficialProductCategory) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
OfficialProductCategory
A Distru standard, system-defined product category that your own product categories can map to
| Property | Description | Type | Required |
|---|---|---|---|
| id | 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. |
string | true |
| name | 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. |
string | true |
Order
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.
| Property | Description | Type | Required |
|---|---|---|---|
| billing_location | A location with its license number inlined, as nested on orders/invoices/purchases | LocationWithLicense | false |
| biotrack_id | 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. |
string | false |
| blaze_payment_type | 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. CASH CREDIT DEBIT COD ACH_TRANSFER CHEQUE OTHER |
string | false |
| buyer_company | 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. | CompanyCompact | false |
| buyer_note | 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) | string | false |
| charges | A collection of Charges | array(Charge) | false |
| combined_order | 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. | CompactOrder | false |
| company | 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. | CompanyCompact | false |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| custom_data | A collection of CustomData | array(CustomField) | true |
| delivered_datetime | The datetime the order was marked Delivered or Completed, or null if it never reached those statuses | string | false |
| delivery_datetime | The datetime the order was / will be delivered, or null if none is set | string | false |
| due_datetime | The datetime by which the customer is expected to pay for this order. Always present — every order has a due date (enforced on write). | string | true |
| external_notes | The "Message to Customer" shown on this order's slips, or null when unset. | string | false |
| id | ID for this order | string | true |
| inserted_datetime | The datetime at which the order was created in Distru | string | true |
| internal_notes | Free-form notes visible only inside Distru, never shown to the customer. Null when unset. | string | false |
| invoices | A collection of the invoices on this order | array(CompactInvoice) | false |
| items | A collection of SalesOrderItems | array(SalesOrderItem) | false |
| leaflink_id | LeafLink's own identifier for this order, or null for orders not synced from LeafLink. Set together with leaflink_order_number. |
string | false |
| leaflink_order_number | 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. |
string | false |
| location | A location with its license number inlined, as nested on orders/invoices/purchases | LocationWithLicense | false |
| menu | 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. | CompactMenu | false |
| metrc_transfer_id | 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. |
integer | false |
| metrc_transfer_template_error | The error explaining why this order's Metrc transfer template failed to sync. Only set while metrc_transfer_template_status is FAILED, null otherwise. |
string | false |
| metrc_transfer_template_id | The ID of the Metrc transfer template Distru created in Metrc for this order, or null if none has been created | integer | false |
| metrc_transfer_template_status | The sync status of this order's Metrc transfer template, or null if no template sync has been requested.
PENDING COMPLETED FAILED |
string | false |
| order_datetime | The datetime on which the order was placed | string | true |
| order_number | The order number as shown in the Distru UI. Distru-assigned and unique per company. | string | true |
| owner | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| payment_term_name | 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. |
string | false |
| returns | A collection of the returns on this order | array(CompactReturn) | false |
| shipping_location | A location with its license number inlined, as nested on orders/invoices/purchases | LocationWithLicense | false |
| status | Where this order is in its lifecycle, which also governs how it affects inventory.
PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
string | true |
| total | 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. | string | true |
| updated_datetime | The datetime at which the order was last updated in Distru | string | true |
OrderChargeRequest
Order charge params
| Property | Description | Type | Required |
|---|---|---|---|
| id | 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. | string | false |
| name | The label for this line (e.g. "Delivery Fee"), shown on the order and its invoice. Required. | string | true |
| percent | 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. |
number | false |
| price | 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. |
number | false |
| tax_id | 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. |
string | false |
| type | 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.CHARGE DISCOUNT |
string | false |
| unit_type | How the line is measured: PERCENT (a percentage of the subtotal, set via percent) or PRICE (a flat amount, set via price). SCREAMING_CASE.PERCENT PRICE |
string | true |
OrderFulfillmentReport
The Order Fulfillment report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows, one per product. | array(OrderFulfillmentReportRow) | true |
| meta | Report-level metadata | OrderFulfillmentReportMeta | true |
OrderFulfillmentReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | 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). |
string | true |
| label | The human-readable column label (e.g. the order number, or Total Units). |
string | true |
OrderFulfillmentReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. | array(OrderFulfillmentReportColumn) | true |
| date_range | The human-readable date range the report covers (the resolved default window, or the requested order_datetime range). |
string | true |
| report | The report identifier; always order_fulfillment for this endpoint. |
string | true |
OrderFulfillmentReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| category | The product's category name, or null when the product has no category. | string | false |
| group | The product's group name, or null when the product has no group. | string | false |
| product | The product name; inactive products carry a prefix marking them inactive. Always present — this is the pivot key each row is built around. | string | true |
| subcategory | The product's subcategory name, or null when the product has no subcategory. | string | false |
| total_units | Total units of this product sold across the matching orders, net of returns (summed order-item quantities minus return quantities). | string | true |
| total_value | 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. |
string | true |
| unit_price | 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. | string | true |
OrderItemRequest
Order item params
| Property | Description | Type | Required |
|---|---|---|---|
| batch_id | 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. |
string | false |
| compliance_quantity | 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. |
number | false |
| id | 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. | string | false |
| is_sample | Marks this line as a sample rather than a normal sale. Defaults to false when omitted. | boolean | false |
| location_id | The location this line item is fulfilled from, as a Distru location ID. Optional. | string | false |
| note | 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. | string | false |
| package_id | 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. |
string | false |
| price_base | 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. |
number | true |
| price_tier_mode | Controls how price tiers 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).AUTO OVERRIDE NONE |
string | false |
| price_tier_version_id | 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. |
string | false |
| product_id | 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. |
string | false |
| quantity | Quantity used on this order item, expressed in the product's unit type | number | true |
OrderResponse
A single order wrapped in a data envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | Order | false |
Orders
A collection of Orders
| Property | Description | Type | Required |
|---|---|---|---|
| data | Orders | array(Order) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Package
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.
| Property | Description | Type | Required |
|---|---|---|---|
| batch_number | A free-text batch number set on the package, separate from any compliance tag. Null when none is set. | string | false |
| compliance_label | 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. | string | false |
| distru_status | The package's inventory lifecycle status. One of:
ACTIVE ASSEMBLING DESTROYED DISCONTINUED FINISHED ONHOLD RETURNING SELLING SOLD TRANSFERRED |
string | true |
| id | ID for this package in Distru | string | true |
| license_id | The ID of the license this package is held under | string | true |
| location_id | The ID of the location where this package is physically stored | string | true |
| metrc_id | The Metrc package ID for this package; null for non-Metrc packages | integer | false |
| quantity | 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. |
string | true |
| quantity_active | 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. |
string | true |
PackageFull
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.
| Property | Description | Type | Required |
|---|---|---|---|
| batch_number | A free-text batch number set on the package, separate from any compliance tag. Null when none is set. | string | false |
| bins | The bins this package is stored in. Only present when bin inventory tracking is enabled for the company. | array(BinCompact) | false |
| biotrack_id | The BioTrack inventory ID for this package; null for non-BioTrack packages | integer | false |
| biotrack_inventory_type_id | The BioTrack inventory type ID for this package. Null for non-BioTrack packages. | integer | false |
| biotrack_net_quantity_per_unit | 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. | string | false |
| biotrack_room_id | The BioTrack room ID where this package is stored. Null for non-BioTrack packages. | integer | false |
| biotrack_status | The package's BioTrack inventory status. Null for non-BioTrack packages. ACTIVE DESTROYED SCHEDULED_FOR_DESTRUCTION SCHEDULED_FOR_TRANSPORT IN_TRANSPORT_BUT_NOT_RECEIVED RECEIVED |
string | false |
| biotrack_usable_weight | 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). | string | false |
| compliance_label | 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. | string | false |
| compliance_product_name | The product/item name as reported by the compliance system (Metrc or BioTrack), or null when none is reported | string | false |
| compliance_strain_name | The strain name as reported by the compliance system (Metrc or BioTrack), or null when none is reported | string | false |
| compliance_transferred_datetime | The datetime this package was transferred out in the compliance system (ISO 8601); null if not transferred out | string | false |
| compliance_type | 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). |
string | false |
| cost_per_unit_actual | 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. |
string | false |
| cost_per_unit_default | 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. |
string | false |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| custom_data | The custom data for this package | array(CustomField) | true |
| description | Free-text description of this package, or null when none was entered | string | false |
| distru_status | The package's inventory lifecycle status. One of:
ACTIVE ASSEMBLING DESTROYED DISCONTINUED FINISHED ONHOLD RETURNING SELLING SOLD TRANSFERRED |
string | true |
| expiration_datetime | ISO 8601 datetime this package expires, or null when none is set | string | false |
| finished_datetime | The datetime this package was finished in the compliance system (ISO 8601); null if not finished | string | false |
| harvest_date | The harvest date for this package as an ISO 8601 date (e.g. "2026-08-20"), or null when none is set | string | false |
| id | ID for this package in Distru | string | true |
| inactivated_datetime | The datetime this package was inactivated (ISO 8601); null while active | string | false |
| inserted_datetime | The datetime this package was created at (ISO 8601) | string | true |
| is_production_batch | True when this package is a new production lot (rather than added to existing inventory). Only applies to Metrc-tracked packages; always false otherwise. | boolean | true |
| is_test_sample | True when this package is a test sample. | boolean | true |
| is_trade_sample | True when this package is a Metrc trade sample. Always false for non-Metrc packages. | boolean | true |
| lab_testing_state | 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. | string | true |
| license | A cannabis license held by a company or tied to a location, identifying it to the state and its compliance system. | License | false |
| license_id | The ID of the license this package is held under | string | true |
| location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | false |
| location_id | The ID of the location where this package is physically stored | string | true |
| metrc_archived_date | The date this package was archived in Metrc — i.e. the moment it was discontinued in Metrc (ISO 8601 date) | string | false |
| metrc_finished_date | The date this package was finished in Metrc (ISO 8601 date) | string | false |
| metrc_id | The Metrc package ID for this package; null for non-Metrc packages | integer | false |
| metrc_production_batch_number | The Metrc production batch number, set only when this package is a production batch (see is_production_batch); otherwise null. |
string | false |
| metrc_received_datetime | The most recent ISO 8601 datetime this package was received via a Metrc transfer, or null when it was never received via a transfer | string | false |
| metrc_received_from_manifest_number | The Metrc manifest number the package was most recently received from, or null when it was not received via a transfer | string | false |
| metrc_source_harvest_names | The Metrc source harvest names for this package. Null when the package is not Metrc-tracked or has no source harvest. | string | false |
| metrc_status | The package's Metrc inventory status. Null for BioTrack-synced packages. ACTIVE INACTIVE ONHOLD |
string | false |
| metrc_transfer_id | The Metrc transfer ID this package is currently on; null if not in transit | integer | false |
| metrc_unit_name | The Metrc unit of measure name for this package (e.g. "Grams"). Null when the package is not Metrc-tracked. | string | false |
| owner | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| packaged_date | The compliance packaged date as an ISO 8601 date (e.g. "2026-08-20"), or null when none is set | string | false |
| primary_test_result | A compact view of the primary test result 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. | PrimaryTestResult | false |
| product | 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). |
Product | true |
| product_id | The ID of the product this package holds | string | true |
| product_unit_quantity | 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. |
string | true |
| product_unit_type | 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. | UnitType | false |
| quantity | 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. |
string | true |
| quantity_active | 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. |
string | true |
| quantity_assembling | 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. |
string | true |
| total_cost_actual | 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. |
string | false |
| total_cost_default | 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. |
string | false |
| unit_type | 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. | UnitType | false |
PackageFullResponse
A single package wrapped in a data envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | PackageFull | false |
Packages
A collection of Packages
| Property | Description | Type | Required |
|---|---|---|---|
| data | Packages | array(PackageFull) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
PackagesWithActiveQuantityByLocation
A location and the product's packages that hold active quantity there.
| Property | Description | Type | Required |
|---|---|---|---|
| location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | true |
| packages | The product's packages held at this location that have active quantity, ordered by id. | array(Package) | true |
Payment
A record of money exchanged — received from a customer against an invoice, or paid to a vendor against a purchase order.
| Property | Description | Type | Required |
|---|---|---|---|
| amount | 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. |
string | true |
| company | 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. | CompanyCompact | false |
| credit_uses | Credits applied toward this invoice payment (an empty array when none were applied). Null for PURCHASE payments. | array(PaymentCreditUse) | false |
| description | Free-text note attached to the payment, or null if none was entered. | string | false |
| fully_paid_with_credits | True when the payment was covered entirely by credits; in that case payment_method is null. Defaults to false. Always present. |
boolean | true |
| id | The payment's ID. Always present. | string | true |
| inserted_datetime | ISO8601 datetime the payment record was created in Distru. Distinct from payment_datetime. Always present. |
string | true |
| invoice | 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. | CompactInvoice | false |
| overpayment_credits | Credits generated from overpaying this invoice payment (an empty array when there was no overpayment). Null for PURCHASE payments. | array(PaymentCredit) | false |
| payment_datetime | 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. |
string | true |
| payment_method | A way payments are made or received (e.g. Cash, Check, Credit Card, Bank Transfer). | PaymentMethod | false |
| payment_number | The payment number as shown in the Distru UI. Unique per company. Always present. | string | true |
| payment_type | 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. |
string | true |
| purchase | A compact representation of the purchase a payment belongs to | PaymentPurchase | false |
| quickbooks_deposit_account_id | ID of the QuickBooks Online deposit account this payment posts to, or null when the payment is not linked to a deposit account. | string | false |
| quickbooks_deposit_account_name | 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. |
string | false |
| quickbooks_sync_enqueued | Whether a QuickBooks Online sync was queued for this payment. Only present on the payment creation response; absent from list and show responses. | boolean | false |
| status | The payment's status. POSTED for a live payment; VOIDED for one that was voided (retained for history rather than deleted). Always present. | string | true |
| updated_datetime | ISO8601 datetime the payment record was last modified in Distru. Equals inserted_datetime until the payment is edited or voided. Always present. |
string | true |
PaymentCredit
A compact representation of a credit related to a payment
| Property | Description | Type | Required |
|---|---|---|---|
| amount | 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. |
string | true |
| credit_number | The credit number as shown in the Distru UI. Always present. | string | true |
| id | ID for this credit. Always present. | string | true |
| source | 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. INVOICE_PAYMENT QB_CREDIT_MEMO QB_PAYMENT RETURN USER |
string | true |
PaymentCreditUse
A credit applied towards an invoice payment
| Property | Description | Type | Required |
|---|---|---|---|
| amount | The amount of credit applied toward the payment, as a decimal string (e.g. "25.00"). Always present. |
string | true |
| credit | A compact representation of a credit related to a payment | PaymentCredit | false |
| id | ID for this credit use. Always present. | string | true |
PaymentMethod
A way payments are made or received (e.g. Cash, Check, Credit Card, Bank Transfer).
| Property | Description | Type | Required |
|---|---|---|---|
| active | True when this payment method is active and can be selected on new payments. Defaults to false. | boolean | true |
| deleted_at | ISO 8601 datetime this payment method was soft-deleted, or null when it has not been deleted | string | false |
| id | ID for this payment method | string | true |
| inserted_datetime | The datetime this payment method was created at | string | true |
| name | Name of the payment method (e.g. "Cash") | string | true |
| qb_payment_method_id | The ID of the matching payment method in QuickBooks Online, or null when this payment method is not mapped to one | string | false |
| type | The payment method type (SCREAMING_CASE). One of CASH, CHECK, CREDIT_CARD, BANK_REMITTANCE, or BANK_TRANSFER. CASH CHECK CREDIT_CARD BANK_REMITTANCE BANK_TRANSFER |
string | true |
| updated_datetime | The datetime this payment method was last updated at | string | true |
PaymentMethodResponse
A single Payment Method
| Property | Description | Type | Required |
|---|---|---|---|
| data | A way payments are made or received (e.g. Cash, Check, Credit Card, Bank Transfer). | PaymentMethod | false |
PaymentMethods
A collection of Payment Methods
| Property | Description | Type | Required |
|---|---|---|---|
| data | Payment Methods | array(PaymentMethod) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
PaymentPurchase
A compact representation of the purchase a payment belongs to
| Property | Description | Type | Required |
|---|---|---|---|
| id | The purchase's ID. Always present. | string | true |
| purchase_number | The purchase number as shown in the Distru UI. Always present. | string | true |
| status | The purchase's status, or null if the purchase has no status set. | string | false |
| total | The purchase's total as a decimal string (e.g. "1200.00"). Defaults to "0". Always present. |
string | true |
PaymentResponse
A single Payment
| Property | Description | Type | Required |
|---|---|---|---|
| data | A record of money exchanged — received from a customer against an invoice, or paid to a vendor against a purchase order. | Payment | false |
PaymentTerm
The agreed timeframe a customer has to pay — for example "Net 30" means payment is due 30 days after the invoice.
| Property | Description | Type | Required |
|---|---|---|---|
| days | Number of days after the invoice date until payment is due. 0 means due the same day. | integer | true |
| id | ID for this payment term | string | true |
| inserted_datetime | The datetime this payment term was created at | string | true |
| locked | 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. |
boolean | true |
| name | Name of the payment term. Unique per company (case-insensitive). | string | true |
| time_of_day | 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". | string | true |
| updated_datetime | The datetime this payment term was last updated at | string | true |
PaymentTerms
A collection of Payment Terms
| Property | Description | Type | Required |
|---|---|---|---|
| data | Payment Terms | array(PaymentTerm) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Payments
A collection of Payments
| Property | Description | Type | Required |
|---|---|---|---|
| data | Payments | array(Payment) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
PdfDownloadUrl
The JSON envelope a PDF download endpoint returns when ?format=url is passed
| Property | Description | Type | Required |
|---|---|---|---|
| data.expires_datetime | ISO 8601 datetime when the signed url expires (e.g. "2026-08-20T00:00:00Z") |
string | false |
| data.url | Temporary signed URL to download the PDF. Stops working once expires_datetime passes; request the endpoint again for a fresh URL. |
string | false |
PlantLifecycleReport
The Plant Lifecycle report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(PlantLifecycleReportRow) | true |
| meta | Report-level metadata | PlantLifecycleReportMeta | true |
PlantLifecycleReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key under which this column's value appears in each data row (e.g. plant_batch_name). |
string | true |
| label | The human-readable column heading (e.g. Plant Batch Name). |
string | true |
PlantLifecycleReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. |
array(PlantLifecycleReportColumn) | true |
| date_range | 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. |
string | false |
| report | The report identifier — always plant_lifecycle. |
string | true |
PlantLifecycleReportRow
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).
| Property | Description | Type | Required |
|---|---|---|---|
| batch_creation_date | 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. |
string | true |
| days_as_batch | 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. | string | true |
| days_veg_to_last_harvest | Whole days from the veg-promotion date to the last harvest date. Null until the batch has both been promoted to veg and harvested. | string | false |
| destroyed_plant_cost | 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. | string | false |
| first_harvest_date | The date of the batch's earliest harvest, formatted MM/DD/YYYY. Null when the batch has no harvest yet. |
string | false |
| harvest_name_s | 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. |
string | false |
| last_harvest_date | The date of the batch's most recent harvest, formatted MM/DD/YYYY. Null when the batch has no harvest yet. |
string | false |
| plant_batch_name | The plant batch name. | string | true |
| plants_destroyed | Count of the batch's plants that have been destroyed. 0 when none. |
string | true |
| plants_harvested | Count of the batch's plants that have been harvested or packaged. 0 when none. |
string | true |
| plants_promoted_to_veg | Count of the batch's plants that have entered the vegetative stage. 0 when none. |
string | true |
| plants_started | Count of plants ever started in this batch (its non-removed plants). 0 when none. |
string | true |
| promoted_to_veg_date | 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. |
string | false |
| strain | The batch's Metrc strain name. Always present — this report covers only Metrc-licensed plant batches, which always carry a strain. | string | true |
| total_cost_batch_stage | 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. | string | false |
| total_cost_veg_to_last_harvest | 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. | string | false |
| total_lifecycle_cost | 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. |
string | false |
| total_lifecycle_days | 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. |
string | true |
PriceTier
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.
| Property | Description | Type | Required |
|---|---|---|---|
| conditions | 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. | PriceTierConditions | true |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| current_version_id | 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 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). |
string | true |
| external_name | Buyer-facing name shown on menus, or null (falls back to name) | string | false |
| id | ID for this price tier | string | true |
| inserted_datetime | When the tier was created (UTC ISO-8601) | string | true |
| is_flat | 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. |
boolean | true |
| menu_mode | Which menus the tier appears on ALL NONE SPECIFIC |
string | true |
| menu_promo_card_background_hex | Background color of a TEXT promo card, as a hex string (e.g. "#FF0000"); null for an IMAGE card | string | false |
| menu_promo_card_emoji | Emoji shown on a TEXT promo card, or null when none is set | string | false |
| menu_promo_card_text_hex | Text color of a TEXT promo card, as a hex string (e.g. "#FFFFFF"); null for an IMAGE card | string | false |
| menu_promo_card_type | The promo card style (SCREAMING_CASE): TEXT renders a colored card using the hex/emoji fields, IMAGE renders an uploaded image instead. TEXT IMAGE |
string | true |
| menu_promo_enabled | Whether the tier renders a promo card on menus. The menu_promo_card_* fields below are only shown to buyers when this is true. |
boolean | false |
| menus | Menus the tier applies to. Populated only when menu_mode is SPECIFIC | array(CompactMenu) | true |
| name | Internal name of the tier | string | true |
| owner | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| percent | The discount percentage (0-100) used when price_or_percent is PERCENT; null when price_or_percent is PRICE |
integer | false |
| price | 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. |
string | false |
| price_or_percent | Whether the discount is a fixed amount or a percentage PRICE PERCENT |
string | true |
| updated_datetime | When the tier was last updated (UTC ISO-8601) | string | true |
| valid_from_datetime | ISO 8601 datetime the tier starts being applicable, or null for no start bound (applicable from any time up to valid_until_datetime) |
string | false |
| valid_until_datetime | ISO 8601 datetime the tier stops being applicable, or null for no end bound (applicable indefinitely from valid_from_datetime) |
string | false |
PriceTierConditions
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.
| Property | Description | Type | Required |
|---|---|---|---|
| min_quantity.quantity | Minimum quantity, e.g. "10" | string | false |
| min_quantity.unit_type | 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. | UnitType | false |
| not_one_of_companies | order_item.order.company must not be one of these | array(CompanyCompact) | true |
| not_one_of_company_relationship_groups | order_item.order.company.group must not be one of these | array(CompanyGroup) | true |
| not_one_of_product_brands | order_item.product.brand must not be one of these | array(CompanyCompact) | true |
| not_one_of_product_categories | order_item.product.category must not be one of these | array(ProductCategoryCompact) | true |
| not_one_of_product_groups | order_item.product.group must not be one of these | array(ProductGroupCompact) | true |
| not_one_of_product_subcategories | order_item.product.subcategory must not be one of these | array(ProductSubcategoryCompact) | true |
| not_one_of_products | order_item.product must not be one of these | array(Product) | true |
| one_of_companies | order_item.order.company must be one of these | array(CompanyCompact) | true |
| one_of_company_relationship_groups | order_item.order.company.group must be one of these | array(CompanyGroup) | true |
| one_of_product_brands | order_item.product.brand must be one of these | array(CompanyCompact) | true |
| one_of_product_categories | order_item.product.category must be one of these | array(ProductCategoryCompact) | true |
| one_of_product_groups | order_item.product.group must be one of these | array(ProductGroupCompact) | true |
| one_of_product_subcategories | order_item.product.subcategory must be one of these | array(ProductSubcategoryCompact) | true |
| one_of_products | order_item.product must be one of these | array(Product) | true |
| total_thc_percentage_range.max | Maximum total THC %, or null | string | false |
| total_thc_percentage_range.min | Minimum total THC %, or null | string | false |
PriceTierConditionsInput
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.
| Property | Description | Type | Required |
|---|---|---|---|
| min_quantity.quantity | Minimum quantity (at least 1) | integer | false |
| min_quantity.unit_type_id | Unit type the minimum is measured in; omit for unit-agnostic | string | false |
| not_one_of_company_ids | order_item.order.company must not be one of these | array(any) | false |
| not_one_of_company_relationship_group_ids | order_item.order.company.group must not be one of these | array(any) | false |
| not_one_of_product_brand_ids | order_item.product.brand must not be one of these | array(any) | false |
| not_one_of_product_category_ids | order_item.product.category must not be one of these | array(any) | false |
| not_one_of_product_group_ids | order_item.product.group must not be one of these | array(any) | false |
| not_one_of_product_ids | order_item.product must not be one of these | array(any) | false |
| not_one_of_product_subcategory_ids | order_item.product.subcategory must not be one of these | array(any) | false |
| one_of_company_ids | order_item.order.company must be one of these | array(any) | false |
| one_of_company_relationship_group_ids | order_item.order.company.group must be one of these | array(any) | false |
| one_of_product_brand_ids | order_item.product.brand must be one of these | array(any) | false |
| one_of_product_category_ids | order_item.product.category must be one of these | array(any) | false |
| one_of_product_group_ids | order_item.product.group must be one of these | array(any) | false |
| one_of_product_ids | order_item.product must be one of these | array(any) | false |
| one_of_product_subcategory_ids | order_item.product.subcategory must be one of these | array(any) | false |
| total_thc_percentage_range.max | Maximum total THC % | number | false |
| total_thc_percentage_range.min | Minimum total THC % | number | false |
PriceTierResponse
A single price tier
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | PriceTier | false |
PriceTierVersion
An immutable snapshot of a price tier, 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.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this price tier version — the value to submit back as an order item's price_tier_version_id |
string | true |
| is_live | 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. |
boolean | true |
| price_tier.id | ID of the live price tier this snapshot belongs to — fetch it via GET /public/v1/price-tiers for its conditions and current state | string | false |
| price_tier.is_flat | 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. |
boolean | false |
| price_tier.name | Internal name of the tier as of this snapshot | string | false |
| price_tier.percent | The discount percentage (0-100) off the order item's price_base used when price_or_percent is PERCENT; null when PRICE |
integer | false |
| price_tier.price | 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. |
string | false |
| price_tier.price_or_percent | Whether the frozen discount is a fixed amount (PRICE) or a percentage (PERCENT) PRICE PERCENT |
string | false |
PriceTiers
A collection of price tiers
| Property | Description | Type | Required |
|---|---|---|---|
| data | Price Tiers | array(PriceTier) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
PrimaryTestResult
A compact view of the primary test result 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.
| Property | Description | Type | Required |
|---|---|---|---|
| cbd_mg_per_unit | CBD content in milligrams per unit, as a decimal string, or null when not measured | string | false |
| cbd_mg_per_unit_total | Total CBD (including its acid precursor) in milligrams per unit, as a decimal string, or null when not measured | string | false |
| cbd_percentage | CBD content as a percentage by weight, as a decimal string, or null when not measured | string | false |
| cbd_percentage_total | Total CBD (including its acid precursor) as a percentage by weight, as a decimal string, or null when not measured | string | false |
| coa_url | Public URL to view/download this test result's Certificate of Analysis (COA), or null when no file is attached | string | false |
| mg_per_unit_type | The unit that the *_mg_per_unit figures are measured against, or null when not set |
string | false |
| name | The name of the test result | string | true |
| thc_mg_per_unit | THC content in milligrams per unit, as a decimal string, or null when not measured | string | false |
| thc_mg_per_unit_total | Total THC (including its acid precursor) in milligrams per unit, as a decimal string, or null when not measured | string | false |
| thc_percentage | THC content as a percentage by weight, as a decimal string, or null when not measured | string | false |
| thc_percentage_total | Total THC (including its acid precursor) as a percentage by weight, as a decimal string, or null when not measured | string | false |
Product
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).
| Property | Description | Type | Required |
|---|---|---|---|
| batches_with_active_quantity_by_location | 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. |
array(BatchesWithActiveQuantityByLocation) | false |
| bill_of_materials | A product's bill of materials (recipe of inputs and additional costs) | BillOfMaterials | false |
| brand | 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. | CompanyCompact | false |
| category | The top-level classification of a product (e.g. Flower, Edibles, Concentrates). Categories are defined per company and can be organized into subcategories. | ProductCategoryCompact | false |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| custom_data | The custom data for this product | array(CustomField) | true |
| deleted_at | ISO 8601 datetime the product was soft-deleted at, or null when the product has not been deleted | string | false |
| 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. |
string | false |
| description_markdown | The same description in Markdown. Null when description is null; populated whenever description is populated. |
string | false |
| external_name | Customer-facing name shown on DistruCommerce menus and the Order Tracker, or null when none is set (it does not fall back to name) |
string | false |
| gross_weight | 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. |
string | false |
| gross_weight_unit_type | 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. | UnitType | false |
| id | ID for this product | string | true |
| images | The images associated with this product, ordered by their id. Empty array when the product has no images. | array(Image) | false |
| inserted_datetime | The datetime this product was created at | string | true |
| inventory_tracking_method | 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).PACKAGE BATCH PRODUCT |
string | true |
| is_active | True when the product is active. False when it has been marked inactive (archived), which hides it from most product pickers. | boolean | true |
| is_featured | True when the product is flagged as featured | boolean | true |
| leaflink_product_id | The LeafLink product ID this product is synced to, or null if not synced | integer | false |
| menu_visibility | 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).DO_NOT_INCLUDE INCLUDE_IN_ALL INCLUDE_IN_SELECT |
string | false |
| menus | 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. |
array(CompactMenu) | false |
| msrp | Manufacturer's suggested retail price, as a decimal string (e.g. "19.99"), or null when not set | string | false |
| name | Human readable name for this product | string | true |
| owner | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| packages_with_active_quantity_by_location | 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. |
array(PackagesWithActiveQuantityByLocation) | false |
| product_group | A named grouping of products, defined per company (e.g. a brand line or product family). | ProductGroupCompact | false |
| quantity_active | 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. |
string | true |
| quantity_active_by_location | 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. |
array(QuantityActiveByLocation) | true |
| quantity_available | 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. |
string | true |
| quantity_available_threshold_max | 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. |
string | false |
| quantity_available_threshold_min | 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. |
string | false |
| quantity_reserved | 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. |
string | true |
| sku | The stock keeping unit (SKU) configured for this product | string | true |
| strain | A cannabis strain (its genetics), such as "Blue Dream". Products can be linked to a strain to carry its name and type. | Strain | false |
| subcategory | A finer classification within a product category (e.g. "Pre-Rolls" under Flower). | ProductSubcategoryCompact | false |
| tags | The tags associated with this product. Empty array when the product has no tags. | array(ProductTagRef) | false |
| total_cannabinoid_unit | 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. |
string | false |
| total_cbd | 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. |
string | false |
| total_thc | 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. |
string | false |
| treez_wholesale_price | The Treez wholesale price of this product, as a decimal string, or null when not set | string | false |
| unit_cost | The cost (purchase price) of one unit of this product, as a decimal string, or null when not set | string | false |
| unit_net_weight | 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. |
string | false |
| unit_net_weight_serving_size_unit_type | 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. | UnitType | false |
| unit_price | The list (sale) price of one unit of this product, as a decimal string (e.g. "25.00") | string | true |
| unit_serving_size | 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. |
string | false |
| unit_type | 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. | UnitType | false |
| units_per_case | The number of units of this product packed in one case, as a decimal string (e.g. "12"), or null when not set | string | false |
| upc | The UPC barcode of this product, or null when not set | string | false |
| updated_datetime | The datetime this product was last updated at | string | true |
| vendor | 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. | CompanyCompact | false |
| wholesale_unit_price | 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. |
number | false |
ProductCategories
A collection of product categories
| Property | Description | Type | Required |
|---|---|---|---|
| data | Product Categories | array(ProductCategory) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
ProductCategory
A product category
| Property | Description | Type | Required |
|---|---|---|---|
| id | The product category's ID. | string | true |
| inserted_datetime | When the product category was created (UTC ISO-8601). | string | true |
| name | The category's display name. Unique within the company. | string | true |
| official_product_category_id | 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. |
string | false |
| subcategories | The subcategories that belong to this category, each as id and name. Always present; an empty array when the category has no subcategories. |
array(ProductSubcategoryCompact) | true |
| updated_datetime | When the product category was last updated (UTC ISO-8601). Equals inserted_datetime until the category is first edited. |
string | true |
ProductCategoryCompact
The top-level classification of a product (e.g. Flower, Edibles, Concentrates). Categories are defined per company and can be organized into subcategories.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this category | string | true |
| name | Human readable name for this category | string | true |
| official_product_category_id | 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. | string | false |
ProductCategoryResponse
A single product category
| Property | Description | Type | Required |
|---|---|---|---|
| data | A product category | ProductCategory | false |
ProductGroup
A product group
| Property | Description | Type | Required |
|---|---|---|---|
| id | The product group's Distru ID. Stable for the life of the group; use it to fetch, update, or delete the group. | string | true |
| inserted_datetime | When the product group was created, as a UTC ISO-8601 timestamp (e.g. 2026-08-20T14:03:00Z). |
string | true |
| name | The group's display name. Unique within the company (case-insensitive). | string | true |
| updated_datetime | When the product group was last updated, as a UTC ISO-8601 timestamp. Equals inserted_datetime until the group is first edited. |
string | true |
ProductGroupCompact
A named grouping of products, defined per company (e.g. a brand line or product family).
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this product group | string | true |
| name | The name of this product group | string | true |
ProductGroupResponse
A single product group
| Property | Description | Type | Required |
|---|---|---|---|
| data | A product group | ProductGroup | false |
ProductGroups
A collection of product groups
| Property | Description | Type | Required |
|---|---|---|---|
| data | Product Groups | array(ProductGroup) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
ProductPosMapping
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.
| Property | Description | Type | Required |
|---|---|---|---|
| blaze_asset_id | Blaze asset (image) id for the product, or null if none was set. Present only when pos_type is BLAZE. |
string | false |
| blaze_product_id | Blaze's own id for the mapped product. Present and non-null only when pos_type is BLAZE; omitted entirely for other POS types. |
string | false |
| blaze_retailer_id | 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. |
string | false |
| dutchie_product_id | 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. |
integer | false |
| dutchie_retailer_id | 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. |
string | false |
| id | ID of this mapping. Use it to fetch or delete the mapping. Always present. | string | true |
| inserted_datetime | When the mapping was created, in UTC. Always present. | string | true |
| pos_type | Which external POS this mapping targets. One of BLAZE, DUTCHIE, or TREEZ (SCREAMING_CASE). Determines which POS-specific fields below are present. Always present.BLAZE DUTCHIE TREEZ |
string | true |
| product_id | ID of the linked Distru product. Always present. | string | true |
| treez_photo_url | URL of the product photo in Treez, or null if none was set. Present only when pos_type is TREEZ. |
string | false |
| treez_product_id | 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. |
string | false |
| treez_retailer_id | 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. |
integer | false |
| updated_datetime | When the mapping was last updated, in UTC. Equals inserted_datetime until the first update. Always present. |
string | true |
ProductPosMappingResponse
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. |
ProductPosMapping | true |
ProductPosMappingsResponse
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | array(ProductPosMapping) | true |
| next_page | 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. |
string | false |
ProductResponse
A single Product
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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). |
Product | false |
ProductSubcategories
A collection of product subcategories
| Property | Description | Type | Required |
|---|---|---|---|
| data | Product Subcategories | array(ProductSubcategory) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
ProductSubcategory
A product subcategory
| Property | Description | Type | Required |
|---|---|---|---|
| category | The top-level classification of a product (e.g. Flower, Edibles, Concentrates). Categories are defined per company and can be organized into subcategories. | ProductCategoryCompact | true |
| id | ID for this product subcategory. Use it as the id for the fetch, upsert, and delete endpoints. |
string | true |
| inserted_datetime | When the product subcategory was created, as a UTC ISO-8601 timestamp (e.g. "2026-08-20T14:30:00Z"). Always present. |
string | true |
| name | The name of the subcategory. Always present; unique within its parent category (case-insensitive). | string | true |
| updated_datetime | 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. |
string | true |
ProductSubcategoryCompact
A finer classification within a product category (e.g. "Pre-Rolls" under Flower).
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this subcategory | string | true |
| name | Human readable name for this subcategory | string | true |
ProductSubcategoryResponse
A single product subcategory
| Property | Description | Type | Required |
|---|---|---|---|
| data | A product subcategory | ProductSubcategory | false |
ProductTagRef
A tag associated with a product
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this tag | string | true |
| name | The name of this tag | string | true |
Products
A collection of Products
| Property | Description | Type | Required |
|---|---|---|---|
| data | Products | array(Product) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Purchase
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.
| Property | Description | Type | Required |
|---|---|---|---|
| billing_location | A location with its license number inlined, as nested on orders/invoices/purchases | LocationWithLicense | false |
| biotrack_id | 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). |
string | false |
| charges | A collection of Charges | array(Charge) | true |
| company | 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. | CompanyCompact | false |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| custom_data | 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. | array(CustomField) | true |
| description | A free-text description of the purchase order. Null if none was set. | string | false |
| due_datetime | The datetime by which the purchase order should be paid (ISO8601 UTC). Null if no due date is set. | string | false |
| id | ID for this purchase order. | string | true |
| inserted_datetime | The datetime at which the order was created in Distru (ISO8601 UTC). | string | true |
| items | A collection of PurchaseOrderItems | array(PurchaseOrderItem) | true |
| location | A location with its license number inlined, as nested on orders/invoices/purchases | LocationWithLicense | false |
| metrc_transfer_id | 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). |
integer | false |
| order_datetime | The datetime on which the order was placed (ISO8601 UTC). Null if unset. Also the field this endpoint sorts by. | string | false |
| owner | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| paid | 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. |
string | true |
| payment_status | How much of the order has been paid, derived from its payments.
NOT_PAID PARTIALLY_PAID FULLY_PAID OVER_PAID |
string | false |
| payments | A collection of the purchase's payments | array(Payment) | true |
| purchase_number | The human-readable purchase order number as shown in the Distru UI. Assigned by Distru; unique within your company. | string | true |
| qb_bill_id | 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. | string | false |
| status | 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.
COMPLETED DELIVERING PENDING PARTIALLY_RECEIVED PROCESSING |
string | true |
| supplier_location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | false |
| total | 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. |
string | true |
| updated_datetime | The datetime at which the order was last modified in Distru (ISO8601 UTC). | string | true |
PurchaseChargeRequest
Purchase charge params
| Property | Description | Type | Required |
|---|---|---|---|
| id | 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. | string | false |
| name | The name of this charge | string | true |
| percent | 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. |
number | false |
| price | 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. |
number | false |
| type | Type of this line item. Note: Tax charges should be sent as CHARGE with a tax_id CHARGE DISCOUNT |
string | true |
| unit_type | Determines if this line is tracked as a percentage or a flat charge PERCENT PRICE |
string | true |
PurchaseItemRequest
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.
| Property | Description | Type | Required |
|---|---|---|---|
| batch_id | 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. |
string | false |
| biotrack_id | 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. |
string | false |
| compliance_quantity | 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. | number | false |
| id | 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. | string | false |
| location_id | The ID of the location this line's inventory is received into. Defaults to the purchase's location_id when omitted. |
string | false |
| metrc_package_id | 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. |
integer | false |
| price | 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. |
number | true |
| product_id | 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. |
string | false |
| quantity | 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. |
number | true |
| received_quantity | 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. |
number | false |
PurchaseOrderHistoryReport
The Purchase Order History report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(PurchaseOrderHistoryReportRow) | true |
| meta | Report-level metadata | PurchaseOrderHistoryReportMeta | true |
PurchaseOrderHistoryReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key this column is stored under in every data row (a slugified version of the label, e.g. purchase_number). |
string | true |
| label | The human-readable column header, e.g. Purchase Number. |
string | true |
PurchaseOrderHistoryReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. | array(PurchaseOrderHistoryReportColumn) | true |
| date_range | The human-readable date range the report covers, after the default 30-day range is applied when order_datetime is omitted. |
string | true |
| report | The report identifier; always purchase_order_history. |
string | true |
PurchaseOrderHistoryReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| amount | The purchase total; 0 when the purchase has no line items. | string | true |
| due_date | The due date as an ISO8601 timestamp in the company's timezone; null when the purchase has no due date. | string | false |
| owner | The purchase owner's full name; null when no owner is assigned. | string | false |
| paid | Total amount paid on the purchase across its non-voided payments; 0 when nothing has been paid. | string | true |
| purchase_date | The purchase (order) date as an ISO8601 timestamp in the company's timezone; null when the purchase has no order date. | string | false |
| purchase_number | The purchase number. | string | true |
| status | The purchase status. One of PENDING, PROCESSING, DELIVERING, PARTIALLY_RECEIVED, COMPLETED — DRAFT never appears here. COMPLETED DELIVERING PENDING PARTIALLY_RECEIVED PROCESSING |
string | true |
| vendor | The vendor (supplier) company name. | string | true |
PurchaseOrderItem
A single product line on a purchase — what is being bought, how much, at what per-unit price, and how much has been received into inventory so far.
| Property | Description | Type | Required |
|---|---|---|---|
| batch | 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. | Batch | false |
| compliance_quantity | 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. | string | false |
| id | ID for this order item | string | true |
| is_sample | True when this line is a sample rather than a bought-for-resale item. Defaults to false. | boolean | true |
| location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | false |
| package | 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. | Package | false |
| price | 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. |
string | true |
| price_base | Per-unit list price for this line, before any discount, as a decimal string. Equal to price on a purchase order item. |
string | true |
| product | 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). |
Product | false |
| quantity | Quantity ordered on this line, in the product's own unit, as a decimal string (e.g. "100"). | string | true |
| received_quantity | 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. |
string | false |
PurchaseResponse
A single purchase order envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | Purchase | false |
Purchases
A collection of Purchases
| Property | Description | Type | Required |
|---|---|---|---|
| data | Purchases | array(Purchase) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
PurchasesByCompanyReport
The Purchases By Company report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(PurchasesByCompanyReportRow) | true |
| meta | Report-level metadata | PurchasesByCompanyReportMeta | true |
PurchasesByCompanyReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key this column appears under on every data row (the slugified label). | string | true |
| label | The human-readable label of the column. | string | true |
PurchasesByCompanyReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | Ordered column definitions for the rows in data, including any appended CompanyRelationship custom-field columns. Always present. |
array(PurchasesByCompanyReportColumn) | true |
| date_range | 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. |
string | true |
| report | The report identifier, always purchases_by_company. |
string | true |
PurchasesByCompanyReportRow
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).
| Property | Description | Type | Required |
|---|---|---|---|
| category | The related company's category, or null when the company has no category set. | string | false |
| last_purchase_date | 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. |
string | false |
| name | The vendor's related company name. Always present. | string | true |
| product_owner | Full name of the sales rep assigned to this vendor, or null when no owner is assigned. | string | false |
| purchase_order_count | 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. |
string | true |
| relationship_type | The vendor's relationship type name, or null when none is assigned. | string | false |
| total_purchases | 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. |
string | true |
PurchasesByProductReport
The Purchases By Product report
| Property | Description | Type | Required |
|---|---|---|---|
| data | One row per purchased product matching the filters. Empty array when no non-draft purchases fall in the range. | array(PurchasesByProductReportRow) | true |
| meta | Report-level metadata returned alongside the report rows. | PurchasesByProductReportMeta | true |
PurchasesByProductReportColumn
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.
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key this column uses in each data row (e.g. quantity_purchased). Matches the property names on a report row. |
string | true |
| label | The human-readable column header as shown in the CSV export (e.g. Quantity Purchased). |
string | true |
PurchasesByProductReportMeta
Report-level metadata returned alongside the report rows.
| Property | Description | Type | Required |
|---|---|---|---|
| columns | Ordered definitions of every column in the report, including any per-company custom-field columns. | array(PurchasesByProductReportColumn) | true |
| date_range | 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. |
string | true |
| report | Stable identifier of this report, always purchases_by_product. |
string | true |
PurchasesByProductReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| category | The product's current category name, or null when the product is uncategorized. | string | false |
| group | The product's current group name, or null when the product has no group. | string | false |
| name | The product name. Always present. When the product is inactive, its name is returned with an inactive marker prepended so it reads as archived. | string | true |
| owner | 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). |
string | false |
| quantity_purchased | 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). |
string | true |
| sale_price | The product's current sale price as a string. Always present and never null. Reflects the product now, not any historical purchase. | string | true |
| sku | The product SKU. Always present, but an empty string (not null) when the product has no SKU set. | string | true |
| subcategory | The product's current subcategory name, or null when none is set. | string | false |
| total_purchased | 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. | string | true |
| unit_cost | 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. | string | false |
| unit_type | 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. |
string | true |
| vendor | 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. | string | false |
| wholesale_price | 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. | string | false |
QuantityActiveByLocation
Active on-hand quantity of an entity held at a single location.
| Property | Description | Type | Required |
|---|---|---|---|
| location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | true |
| quantity | Active on-hand quantity at this location, as a decimal string (e.g. "100"). Always positive. | string | true |
RelationshipType
How a company relates to your business — whether they are a customer you sell to, a vendor you buy from, or both.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this relationship type | string | true |
| name | Name of the relationship type (e.g. Customer, Vendor) | string | true |
Return
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.
| Property | Description | Type | Required |
|---|---|---|---|
| company | 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. | CompanyCompact | false |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | true |
| credits | Customer credits generated from this return, in compact form. Empty array when the return did not create a credit. | array(CompactCredit) | true |
| custom_data | Custom field values for this return, keyed by custom field id. Empty object when none are set. | map | true |
| description | Free-text note on the return, or null. | string | false |
| id | Distru ID for this return. | string | true |
| inserted_datetime | The datetime the return was created in Distru. | string | true |
| invoice_numbers | Invoice numbers of the associated order, sorted ascending. Empty array when the return has no order, or the order has no invoices. | array(any) | true |
| items | The line items on this return. | array(ReturnItem) | true |
| location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | false |
| order | 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. | CompactOrder | false |
| order_quantity | 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. |
string | false |
| owner | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| qb_credit_memo_id | 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. | string | false |
| return_datetime | The business date of the return. May differ from inserted_datetime (when the record was created). Null if not set. |
string | false |
| return_number | Human-readable return number shown in the Distru UI. Unique within your company. | string | true |
| return_quantity | Total quantity returned across all items on this return, as a decimal string (e.g. "4"). Null for a generic return with no order. |
string | false |
| return_type | 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.Full Return Partial Return |
string | false |
| status | 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. PROCESSING SHIPPED RECEIVED COMPLETED |
string | true |
| total | Total monetary value of the returned line items, summed from the return's items. Defaults to 0 when the return has no items. | number | true |
| updated_datetime | The datetime the return was last modified in Distru. | string | true |
ReturnItem
A single line on a return — how much of an order line item was sent back.
| Property | Description | Type | Required |
|---|---|---|---|
| id | Distru ID for this return item. | string | true |
| order_item | 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. | CompactOrderItem | false |
| quantity | 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. | number | true |
| waste | 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. |
boolean | true |
ReturnResponse
A single return envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | Return | false |
Returns
A collection of Returns
| Property | Description | Type | Required |
|---|---|---|---|
| data | Returns | array(Return) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Role
A permission role that determines what a user can do in Distru.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this role | string | true |
| name | Name of the role (e.g. Admin, Sales) | string | true |
SalesByCompanyReport
The Sales By Company report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(SalesByCompanyReportRow) | true |
| meta | Report-level metadata | SalesByCompanyReportMeta | true |
SalesByCompanyReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key used for this column in each data row | string | true |
| label | The human-readable label of the column | string | true |
SalesByCompanyReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. |
array(SalesByCompanyReportColumn) | true |
| date_range | 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. |
string | true |
| report | The report identifier, always sales_by_company |
string | true |
SalesByCompanyReportRow
A single row of the Sales By Company report. Companies with CompanyRelationship custom fields will see additional keys.
| Property | Description | Type | Required |
|---|---|---|---|
| category | The customer's category, or null when unset. | string | false |
| last_order_date | 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. |
string | true |
| name | The customer's company name | string | true |
| order_count | 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. | string | true |
| owner | The full name of the sales rep assigned to this customer, or null when no owner is assigned. | string | false |
| relationship_type | The name of the customer's relationship type, or null when none is assigned. | string | false |
| total_received | The sum of payments applied to the customer's counted orders, as a currency amount. | string | true |
| total_sales | The sum of the counted orders' totals minus any returns against them. Can be negative when returns exceed order totals. | string | true |
SalesByProductReport
The Sales By Product report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(SalesByProductReportRow) | true |
| meta | Report-level metadata | SalesByProductReportMeta | true |
SalesByProductReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | 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. |
string | true |
| label | The human-readable column heading, e.g. Quantity Sold. |
string | true |
SalesByProductReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | The report's column definitions in order, including any appended Product custom-field columns. Use these keys to read each data row. | array(SalesByProductReportColumn) | true |
| date_range | The resolved date range the report covers, as a display string (reflects the last-30-days default when order_datetime was omitted). |
string | true |
| report | The report identifier, always sales_by_product. |
string | true |
SalesByProductReportRow
A single row of the Sales By Product report. Companies with Product custom fields will see additional keys.
| Property | Description | Type | Required |
|---|---|---|---|
| category | The product's category name. Always present (every product is categorized). | string | true |
| group | The product's group name, or null when the product has none. | string | false |
| name | The product name. Inactive products are prefixed to mark them as such. | string | true |
| product_owner | The product owner's full name, or null when no owner is set. | string | false |
| quantity_sold | Units sold over the date range, net of returns. Can be negative when returns exceed sales. | string | true |
| sale_price | The product's sale price. Always present. | string | true |
| shipped_from_license | The license number the sold items shipped from, or null when none applies. | string | false |
| sku | 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. | string | true |
| subcategory | The product's subcategory name, or null when none is set. | string | false |
| total_sales | Total sales value over the date range, net of returns. Can be negative when returns exceed sales. | string | true |
| unit_cost | The product's unit cost, or null when no cost is recorded. | string | false |
| unit_type | The product's unit type name. Always present. | string | true |
| upc | 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. | string | false |
| vendor | The product's vendor (supplier) company name. Always present. | string | true |
| wholesale_price | The product's wholesale price, or null when no wholesale price is set. | string | false |
SalesByUserReport
The Sales By User report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(SalesByUserReportRow) | true |
| meta | Report-level metadata | SalesByUserReportMeta | true |
SalesByUserReportColumn
Definition of one column in the report, pairing its data-row key with a display label.
| Property | Description | Type | Required |
|---|---|---|---|
| key | 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). |
string | true |
| label | The human-readable label of the column, suitable for a table header (for example Sales (Pre-Tax)). |
string | true |
SalesByUserReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. |
array(SalesByUserReportColumn) | true |
| date_range | 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). |
string | true |
| report | The report identifier, always sales_by_user. |
string | true |
SalesByUserReportRow
A single row of the Sales By User report.
| Property | Description | Type | Required |
|---|---|---|---|
| leaderboard_rank | The user's rank by total sales within this report, starting at 1 for the top seller. Rows are always returned in this order. | string | true |
| order_count | How many of the user's orders fall in the report's date range and status set. | string | true |
| sales_pre_tax | The user's pre-tax sales total for the counted orders, net of returns. Excludes tax; a returned order lowers this figure. | string | true |
| total_sales | The user's total sales for the counted orders, net of returns. Same orders as sales_pre_tax but including tax. |
string | true |
| user | The user's (sales rep's) name | string | true |
SalesOrderHistoryReport
The Sales Order History report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(SalesOrderHistoryReportRow) | true |
| meta | Report-level metadata | SalesOrderHistoryReportMeta | true |
SalesOrderHistoryReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | 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. | string | true |
| label | The human-readable column label as shown in the CSV export (e.g. "Order Date", "Discounts (taxes not included)") | string | true |
SalesOrderHistoryReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | 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. |
array(SalesOrderHistoryReportColumn) | true |
| date_range | The human-readable date range the report covers, reflecting the resolved order_datetime filter (or the last-30-days default when none was given) |
string | true |
| report | The report identifier; always "sales_order_history" | string | true |
SalesOrderHistoryReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| charges_taxes_not_included | Total non-tax charges on the order, excluding taxes; 0 when none | string | true |
| customer | The customer (company relationship) name, or null when the order has no linked customer. |
string | false |
| delivery_date | The delivery date as a display-formatted string in the company's timezone, or null when the order has no delivery date. |
string | false |
| delivery_date_utc | The same delivery date in UTC, or null when the order has no delivery date. |
string | false |
| discounts_taxes_not_included | Total discounts applied to the order, excluding taxes; 0 when none | string | true |
| due_date | The due date as a display-formatted string in the company's timezone. Always present — every order has a due date (enforced on write). | string | true |
| due_date_utc | The same due date in UTC. Always present. | string | true |
| order_date | The order date as a display-formatted string in the company's timezone, e.g. "01/15/2026" (not ISO8601) | string | true |
| order_date_utc | The same order date as a display-formatted string in UTC | string | true |
| order_number | 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. | string | true |
| outstanding | Unpaid balance: total minus returns minus payments; 0 when fully settled | string | true |
| owner | The order owner's name, or null when the order has no owner. |
string | false |
| paid | Total payments applied to the order; 0 when none | string | true |
| returns | Total value of returns against the order; 0 when none | string | true |
| status | 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. PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
string | true |
| subtotal | Sum of the order's line-item quantity times price, before taxes, charges, and discounts; 0 when the order has no items | string | true |
| taxes | Total tax charges on the order; 0 when none | string | true |
| total | The order grand total (subtotal plus charges and taxes, minus discounts); 0 when the order is empty | string | true |
SalesOrderItem
A single product line on a sales order — what is being sold, how much, at what price, and which inventory (batch/package) fulfills it.
| Property | Description | Type | Required |
|---|---|---|---|
| batch | 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. | Batch | false |
| compliance_quantity | 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. | string | false |
| cost_per_unit | Actual cost per unit — total_cost_actual divided by this order item's quantity. |
string | false |
| cost_per_unit_default | Default (standard) cost per unit — total_cost_default divided by this order item's quantity. |
string | false |
| id | ID for this order item | string | true |
| inserted_datetime | The datetime this order item was created at | string | true |
| is_sample | True if this order item is a sample given away rather than sold. | boolean | true |
| leaflink_id | 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. | integer | false |
| location | A compact reference to a location as nested inside another entity in Distru. Use its id to fetch the full location from the locations endpoint. |
LocationCompact | false |
| note | Free-text note on this order item, or null when none was entered. | string | false |
| package | 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. | Package | false |
| price | 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. |
string | true |
| price_base | The per-unit list price of this order item before any price tier discount, as a decimal string (e.g. "30.00"). | string | true |
| price_tier_mode | How price tiers determine this order item's price (SCREAMING_CASE):
AUTO OVERRIDE NONE |
string | true |
| price_tier_version | An immutable snapshot of a price tier, 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. | PriceTierVersion | false |
| product | 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). |
Product | false |
| quantity | Quantity sold on this order item, expressed in the product's unit type, as a decimal string (e.g. "10") | string | true |
| returned_quantity | 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). |
string | false |
| thc_percentage_total | 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. | string | false |
| total_cost_actual | 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. |
string | false |
| total_cost_default | 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. |
string | false |
SalesOrderItemHistoryReport
The Sales Order Item History report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows, one per matching sales order line item; empty when nothing matches the filters. | array(SalesOrderItemHistoryReportRow) | true |
| meta | Report-level metadata | SalesOrderItemHistoryReportMeta | true |
SalesOrderItemHistoryReportColumn
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).
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key this column appears under in every data row (e.g. order_number). |
string | true |
| label | The human-readable column heading (e.g. Order Number). |
string | true |
SalesOrderItemHistoryReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | The ordered column definitions for this response, including any compliance-only and custom-field columns. Use these keys to read the data rows. | array(SalesOrderItemHistoryReportColumn) | true |
| date_range | 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. |
string | true |
| report | The report identifier, always sales_order_item_history. |
string | true |
SalesOrderItemHistoryReportRow
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.
| Property | Description | Type | Required |
|---|---|---|---|
| batch_number | The batch number the line item was sourced from, or null if not batch-sourced. |
string | false |
| brand | The product's brand name, or null if the product has no brand. |
string | false |
| brand_id | The brand's ID, or null if the product has no brand. |
string | false |
| category | The product's category. | string | true |
| customer | The order's customer name, or null if the order has no customer. |
string | false |
| customer_id | The customer's ID, or null if the order has no customer. |
string | false |
| default_unit_cost | 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. |
string | false |
| default_unit_price | The product's default unit price as a string. | string | true |
| default_wholesale_price | The product's default wholesale price as a string, or null if unset. |
string | false |
| delivery_date | The order's delivery date in the company's timezone, or null if the order has no delivery date. |
string | false |
| delivery_date_utc | The same delivery date in UTC, or null if the order has no delivery date. |
string | false |
| due_date | The order's due date in the company's timezone, or null if the order has no due date. |
string | false |
| due_date_utc | The same due date in UTC, or null if the order has no due date. |
string | false |
| group | The product's group, or null if the product is in no group. |
string | false |
| invoice_numbers | Comma-separated invoice numbers linked to this line item's order, sorted for a stable order; null if the order has no invoices. |
string | false |
| line_item_id | The line item's ID. | string | true |
| order_date | The order date, formatted in the company's timezone. | string | true |
| order_date_utc | The same order date, formatted in UTC. | string | true |
| order_id | The ID of the order this line item belongs to. | string | true |
| order_item_price | The line item's unit price as a string. | string | true |
| order_number | 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. |
string | true |
| product | The product's name. Inactive products carry a prefix on the name. | string | true |
| product_id | The product's ID. | string | true |
| product_sku | The product's SKU, or an empty string if none is set. | string | false |
| quantity | The line item's ordered quantity as a string. | string | true |
| returned_quantity | The quantity returned on this line item as a string; 0 when nothing was returned. | string | false |
| sales_rep | The order owner's name, treated as the sales rep; null if unassigned. |
string | false |
| source_package | 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. |
string | false |
| status | The order's status as a SCREAMING_CASE token. PENDING PROCESSING READY_TO_SHIP DELIVERING DELIVERED COMPLETED CANCELED |
string | true |
| subcategory | The product's subcategory, or null if none. |
string | false |
| upc | The product's UPC, or null if none is set. |
string | false |
| vendor | The product's vendor name. | string | true |
| vendor_id | The vendor's ID. | string | true |
SalesOrderTaxReport
The Sales Order Tax report
| Property | Description | Type | Required |
|---|---|---|---|
| data | The report rows | array(SalesOrderTaxReportRow) | true |
| meta | Report-level metadata | SalesOrderTaxReportMeta | true |
SalesOrderTaxReportColumn
| Property | Description | Type | Required |
|---|---|---|---|
| key | The key this column appears under in every data row (e.g. total_tax). |
string | true |
| label | The human-readable column header (e.g. "Total Tax"). | string | true |
SalesOrderTaxReportMeta
Report-level metadata
| Property | Description | Type | Required |
|---|---|---|---|
| columns | The report's ordered column definitions, pairing each row key with its display label. | array(SalesOrderTaxReportColumn) | true |
| date_range | 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. |
string | true |
| report | The report identifier; always sales_order_tax for this endpoint. |
string | true |
SalesOrderTaxReportRow
A single aggregated row of the Sales Order Tax report: all matching orders' charges for one unique tax name and rate, summed together.
| Property | Description | Type | Required |
|---|---|---|---|
| tax_rate | 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. |
string | false |
| tax_type | The name of the tax charge (e.g. "Cannabis Excise Tax"). May be null when the underlying tax charge has no name. | string | false |
| total_tax | 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. |
string | true |
SplitPackageOutput
A single output package produced by the split
| Property | Description | Type | Required |
|---|---|---|---|
| batch_number | Distru batch number stored on the output package. | string | false |
| bin_ids | 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. | array(any) | false |
| compliance_label | The Metrc tag for the new package. Must be an available tag in the source package's license. | string | true |
| copy_custom_data_from_input | When true, copies the source package's custom field values onto the output package. | boolean | false |
| costs | Costs to apply to the output package (see CostEntryInput). Optional. | array(CostEntryInput) | false |
| expiration_date | Expiration date reported to Metrc, e.g. "2027-08-19". | string | false |
| input_compliance_quantity | Amount drawn from the source package, in the source package's compliance unit. Must be > 0. | number | true |
| location_id | The output location ID. Must be in the same Metrc license as the source package. | string | true |
| metrc_item_id | 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. | integer | false |
| metrc_notes | Notes sent to Metrc as the output package's note when it is created (max 255 characters). | string | false |
| metrc_production_batch_number | When set, flags the output as a Metrc production batch with this batch number. | string | false |
| output_compliance_quantity | 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. | number | true |
| package_date | The output package's packaged date. Defaults to today when omitted. | string | false |
| product_id | The output product ID. Must be package-tracked. | string | true |
| use_same_item | When true, the output package reuses the source package's Metrc item and metrc_item_id must be omitted. | boolean | false |
SplitPackageRequest
A Metrc source package and the output packages to split it into
| Property | Description | Type | Required |
|---|---|---|---|
| outputs | The output packages to create, between 1 and 300 | array(SplitPackageOutput) | true |
| source_package_id | The package to split. Must be package-tracked and in a Metrc license. | string | true |
StockAdjustment
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.
| Property | Description | Type | Required |
|---|---|---|---|
| batch_id | The ID of this adjustment's batch, or null when the adjusted product is not batch-tracked | string | false |
| completion_datetime | ISO 8601 datetime this adjustment took effect on inventory | string | true |
| compliance_quantity | 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). |
string | false |
| compliance_unit_type | 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. | UnitType | false |
| creator | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
| description | A free-text note explaining this adjustment, or null when none was entered | string | false |
| id | ID for this stock adjustment | string | true |
| inserted_datetime | ISO 8601 datetime this adjustment was created at | string | true |
| license_id | ID of the license this adjustment is associated with — taken from the adjustment's location. Null when there is no location or the location has no license. | string | false |
| location_id | ID of the location this adjustment is associated with, or null when none is set | string | false |
| owner_id | The ID of the Distru user who owns this adjustment, or null when unassigned | string | false |
| package_id | The ID of this adjustment's package, or null when the adjusted product is not package-tracked | string | false |
| product_id | The ID of this adjustment's product. Populated regardless of the product's inventory tracking method. | string | true |
| quantity | 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"). | string | true |
| reason | Why the inventory was adjusted. Returned lowercase (not SCREAMING_CASE), one of: "waste", "stolen", "damaged", "fire", "write-off", "expired", "lab-testing", "revaluation", or "other". damaged expired fire lab-testing other revaluation stolen waste write-off |
string | true |
| total_cost | The total cost of this adjustment, as a decimal string, or null when no cost was recorded | string | false |
| unit_cost | 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. |
string | false |
| unit_type | 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. | UnitType | false |
| updated_datetime | ISO 8601 datetime this adjustment was last modified at | string | true |
StockAdjustmentResponse
A single stock adjustment envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. | StockAdjustment | false |
StockAdjustments
A collection of Stock Adjustments
| Property | Description | Type | Required |
|---|---|---|---|
| data | Stock Adjustments | array(StockAdjustment) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Strain
A cannabis strain (its genetics), such as "Blue Dream". Products can be linked to a strain to carry its name and type.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this strain | string | true |
| inserted_datetime | The datetime this strain was created at | string | true |
| name | Name of the strain | string | true |
| strain_type | The genetic classification of the strain. Null when unset. INDICA INDICA_DOMINANT SATIVA SATIVA_DOMINANT HYBRID HIGH_CBD |
string | false |
| updated_datetime | The datetime this strain was last updated at | string | true |
StrainResponse
A single Strain
| Property | Description | Type | Required |
|---|---|---|---|
| data | A cannabis strain (its genetics), such as "Blue Dream". Products can be linked to a strain to carry its name and type. | Strain | false |
Strains
A collection of Strains. Note: This endpoint returns eventually consistent data, with changes taking up to 1 second to propagate in responses.
| Property | Description | Type | Required |
|---|---|---|---|
| data | Strains | array(Strain) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Tag
A tag
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this tag. | string | true |
| name | The tag's display name. May be null when the tag was created without one. | string | false |
TagResponse
A single tag envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | A tag | Tag | false |
Tags
A collection of tags
| Property | Description | Type | Required |
|---|---|---|---|
| data | Tags | array(Tag) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Tax
A tax
| Property | Description | Type | Required |
|---|---|---|---|
| description | Free-text note describing the tax. Null when no description was set. | string | false |
| id | ID for this tax. | string | true |
| inserted_datetime | When the tax was created, as a UTC ISO-8601 timestamp (e.g. 2024-01-15T09:30:00Z). | string | true |
| name | The tax's display name, unique within the company. | string | true |
| qb_account_id | 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. | string | false |
| qb_product_id | 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. | string | false |
| tags | The tags associated with this tax, each rendered with only its id and name. Always present; an empty array when the tax has no tags. |
array(Tag) | true |
| tax_applied_after_charges | Controls the base amount this tax is applied to on every order and 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. | boolean | true |
| tax_applied_after_price_tiers | Controls whether this tax is applied before or after price tier (tiered/volume) pricing adjustments on the order and 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. |
boolean | true |
| tax_code | The tax code, unique within the company. | string | true |
| tax_rate_percent | The tax rate as a percentage applied to the order and invoice line totals this tax covers, e.g. 8.25 means 8.25%. Always present; never null. | number | true |
| updated_datetime | When the tax was last updated, as a UTC ISO-8601 timestamp (e.g. 2024-01-15T09:30:00Z). | string | true |
TaxResponse
A single tax envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | A tax | Tax | false |
Taxes
A collection of taxes
| Property | Description | Type | Required |
|---|---|---|---|
| data | Taxes | array(Tax) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
TestResult
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.
| Property | Description | Type | Required |
|---|---|---|---|
| additional_test_results | 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. |
AdditionalTestResult | true |
| batch_id | The ID of the batch this test result belongs to, or null when it is attached to a package instead (see package_id) |
string | false |
| biotrack_id | The test result's ID in BioTrack. A BioTrack identifier, not a Distru ID. Null when the result did not come from BioTrack. | string | false |
| cbd_mg_per_unit | CBD content in milligrams per unit, as a decimal string, or null when not measured | string | false |
| cbd_percentage | CBD content as a percentage by weight, as a decimal string, or null when not measured | string | false |
| coa_url | Public URL to view/download this test result's Certificate of Analysis (COA), or null when no file is attached | string | false |
| id | ID for this test result | string | true |
| inserted_datetime | ISO 8601 datetime this test result was created at | string | true |
| is_primary | True when this is the primary test result for its batch or package (the one whose potency figures represent the product) | boolean | true |
| lab_license_number | The license number of the lab that performed this test, or null when not set | string | false |
| lab_name | The name of the lab that performed this test, or null when not set | string | false |
| metrc_id | The test result's ID in Metrc. A Metrc identifier, not a Distru ID. Null when the result did not come from Metrc. | integer | false |
| mg_per_unit_type | The unit that the *_mg_per_unit figures are measured against (e.g. the per-unit size), or null when not set |
string | false |
| name | The name of the test result | string | true |
| package_id | The ID of the package this test result belongs to, or null when it is attached to a batch instead (see batch_id) |
string | false |
| release_date | The date this test result was released (e.g. "2026-08-20"), or null when not set | string | false |
| thc_mg_per_unit | THC content in milligrams per unit, as a decimal string, or null when not measured | string | false |
| thc_percentage | THC content as a percentage by weight, as a decimal string, or null when not measured | string | false |
| total_cbd_mg_per_unit | Total CBD (including its acid precursor) in milligrams per unit, as a decimal string, or null when not measured | string | false |
| total_cbd_percentage | Total CBD (including its acid precursor) as a percentage by weight, as a decimal string, or null when not measured | string | false |
| total_thc_mg_per_unit | Total THC (including its acid precursor) in milligrams per unit, as a decimal string, or null when not measured | string | false |
| total_thc_percentage | Total THC (including its acid precursor) as a percentage by weight, as a decimal string, or null when not measured | string | false |
| updated_datetime | ISO 8601 datetime this test result was last updated at | string | true |
TestResultResponse
A single test result envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | 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. |
TestResult | false |
TestResults
A collection of Test Results
| Property | Description | Type | Required |
|---|---|---|---|
| data | Test Results | array(TestResult) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
UnitType
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.
| Property | Description | Type | Required |
|---|---|---|---|
| id | ID for this unit type | string | true |
| name | Human readable name for this unit type | string | true |
UnitTypeFull
A unit type
| Property | Description | Type | Required |
|---|---|---|---|
| active | 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. | boolean | true |
| category | 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.COUNT VOLUME WEIGHT |
string | false |
| id | ID of this unit type. Stable across renames; use it as the id for the fetch endpoint. |
string | true |
| inserted_datetime | When the unit type was created, as a UTC ISO-8601 timestamp (e.g. "2026-08-20T14:30:00Z"). Always present. |
string | true |
| locked | 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. |
boolean | true |
| name | Display name of the unit type (e.g. "Gram", "Each"). Always present and unique within your company (case-insensitive). |
string | true |
| qty_per_si_unit | 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. |
string | false |
| updated_datetime | 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. |
string | true |
UnitTypeFullResponse
A single unit type envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | A unit type | UnitTypeFull | false |
UnitTypes
A collection of unit types
| Property | Description | Type | Required |
|---|---|---|---|
| data | Unit Types | array(UnitTypeFull) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
UpsertAssemblyCost
A cost added directly to an assembly output
| Property | Description | Type | Required |
|---|---|---|---|
| action | CREATE, UPDATE, or DELETE. Required. CREATE UPDATE DELETE |
string | true |
| cost_per_unit | The per-unit rate applied to this cost. Optional. | number | false |
| cost_type_id | 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. | string | false |
| description | A free-text description for this cost. Optional. | string | false |
| id | The cost to update or delete. Required for UPDATE and DELETE; omit for CREATE. | string | false |
| quantity | The quantity of this cost; must be greater than 0. Required when creating. | number | false |
UpsertAssemblyInput
An ingredient consumed by the assembly to produce an output.
A 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):
- Package-tracked:
package_id+compliance_quantity. The product and location are derived from the package. - Batch-tracked:
batch_id+quantity+location_id. The product is derived from the batch. - Product-tracked:
product_id+quantity+location_id.
A 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.
| Property | Description | Type | Required |
|---|---|---|---|
| action | CREATE, UPDATE, or DELETE. Required. CREATE UPDATE DELETE |
string | true |
| batch_id | 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. | string | false |
| compliance_quantity | The consumed quantity in the package's compliance unit (Metrc). Required for a fulfilled package-tracked input; must be omitted otherwise (including DRAFT). | number | false |
| id | The input to update or delete. Required for UPDATE and DELETE; omit for CREATE. | string | false |
| location_id | 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). | string | false |
| package_id | 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. | string | false |
| product_id | 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. | string | false |
| quantity | 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). |
number | false |
| status | PENDING, COMPLETED, or DRAFT. Required when creating. DRAFT reserves product-level inventory without picking a specific lot; PENDING/COMPLETED consume specific inventory. COMPLETED DRAFT PENDING |
string | false |
UpsertAssemblyMetrcProcessingJob
The Metrc processing job details for an assembly
| Property | Description | Type | Required |
|---|---|---|---|
| name | 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. |
string | false |
| notes | The Metrc processing job notes. Required to complete (status COMPLETED) a job. Editable after the job is created. | string | false |
| type_id | The Metrc ID of an existing Metrc processing job type. Required together with name. Permanent once set — cannot be changed on a later update. |
integer | false |
| waste | 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. | UpsertAssemblyMetrcProcessingJobWaste | false |
UpsertAssemblyMetrcProcessingJobWaste
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.
| Property | Description | Type | Required |
|---|---|---|---|
| count_quantity | Count-based waste. Required together with count_unit_name. |
number | false |
| count_unit_name | The Metrc unit name for count_quantity, e.g. "Each". |
string | false |
| volume_quantity | Volume-based waste. Required together with volume_unit_name. |
number | false |
| volume_unit_name | The Metrc unit name for volume_quantity, e.g. "Milliliters". |
string | false |
| weight_quantity | Weight-based waste. Required together with weight_unit_name. |
number | false |
| weight_unit_name | The Metrc unit name for weight_quantity, e.g. "Grams". |
string | false |
UpsertAssemblyOutput
An output produced by the assembly. What you send depends on how the output product's inventory is tracked:
- Product-tracked (non-compliance):
product_id+quantity+location_id. The batch is the product's existing batch (derived — omitbatch_id). - Batch-tracked (non-compliance):
product_id+quantity+location_id. Optionally setbatch_idto use an existing batch, or omit it and a new batch is created on completion (named bybatch_number). - Package-tracked (Metrc):
product_id+location_id(a Metrc-licensed facility) +compliance_quantity+compliance_label(the Metrc tag) + a Metrc item (metrc_item_idoruse_same_item=true). Omitbatch_id(derived from the product's batch) andquantity(derived fromcompliance_quantity). Distru creates the package when the output is completed.
These 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.
| Property | Description | Type | Required |
|---|---|---|---|
| action | CREATE, UPDATE, or DELETE. Required. CREATE UPDATE DELETE |
string | true |
| batch_id | 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). |
string | false |
| batch_number | 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. |
string | false |
| bin_ids | 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. | array(any) | false |
| compliance_label | 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. | string | false |
| compliance_quantity | 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. |
number | false |
| copy_custom_data_from_input | Copy custom field values from the input onto this output. | boolean | false |
| costs | The costs added directly to this output. | array(UpsertAssemblyCost) | false |
| expiration_date | Package-tracked (Metrc) outputs only. The expiration date reported to Metrc for the created package, e.g. "2027-08-19". | string | false |
| id | The output to update or delete. Required for UPDATE and DELETE; omit for CREATE. | string | false |
| inputs | The inputs consumed to produce this output. | array(UpsertAssemblyInput) | false |
| is_finished_good | Package-tracked (Metrc) outputs only. When true, Distru flags the created package in Metrc as a Finished Good. | boolean | false |
| is_trade_sample | Package-tracked (Metrc) outputs only. Marks the created package as a Metrc trade sample. | boolean | false |
| location_id | The location the output is produced into. Required when creating. | string | false |
| metrc_item_id | Package-tracked (Metrc) outputs only. The Metrc item for the created package. Provide this or use_same_item=true (mutually exclusive). |
integer | false |
| metrc_location_id | 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. | integer | false |
| metrc_notes | Package-tracked (Metrc) outputs only. Notes sent to Metrc when the package is created (max 255 characters). | string | false |
| metrc_production_batch_number | Package-tracked (Metrc) outputs only. Flags the created package in Metrc as a production batch and gives it this production batch number. | string | false |
| package_date | Package-tracked (Metrc) outputs only. The packaged date reported to Metrc for the created package. Defaults to today when omitted. | string | false |
| product_id | The product this output produces. Required when creating. | string | false |
| quantity | 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. |
number | false |
| status | 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. PENDING COMPLETED |
string | false |
| use_same_item | 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. |
boolean | false |
UpsertAssemblyRequest
An assembly to create, update, or delete, with its outputs, inputs, and costs
| Property | Description | Type | Required |
|---|---|---|---|
| action | CREATE, UPDATE, or DELETE. Required. DELETE removes the assembly and all of its outputs, inputs, and costs. CREATE UPDATE DELETE |
string | true |
| description | A free-text description for this assembly. Editable at any status. | string | false |
| estimated_start_datetime | 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. |
string | false |
| estimated_work_hours | 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. |
integer | false |
| estimated_work_minutes | The minutes portion of the estimated work time; must be 0 or greater. Pairs with estimated_work_hours (see above). Editable at any status. |
integer | false |
| id | The assembly to update or delete. Required for UPDATE and DELETE; omit for CREATE. | string | false |
| metrc_processing_job | The Metrc processing job details for an assembly | UpsertAssemblyMetrcProcessingJob | false |
| outputs | 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. |
array(UpsertAssemblyOutput) | false |
| owner_id | The ID of the user that owns this assembly. Optional. Editable at any status. | string | false |
| status | 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).PENDING COMPLETED |
string | false |
UpsertCredit
Parameters for creating or updating a credit
| Property | Description | Type | Required |
|---|---|---|---|
| amount | 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. |
number | false |
| company_id | 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. | string | false |
| external_note | A note on this credit, visible to the customer. Omit to leave unchanged on update; send null to clear. | string | false |
| id | 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. | string | false |
| internal_note | An internal note on this credit, not shown to the customer. Omit to leave unchanged on update; send null to clear. | string | false |
| owner_id | 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. | string | false |
| quickbooks_sales_item_id | 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. | string | false |
UpsertProductPosMapping
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.
| Property | Description | Type | Required |
|---|---|---|---|
| blaze_asset_id | 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. |
string | false |
| blaze_product_id | 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. |
string | false |
| blaze_retailer_id | Distru ID of the connected Blaze retailer to scope the mapping to. Provide together with blaze_product_id. |
string | false |
| dutchie_product_id | 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. |
integer | false |
| dutchie_retailer_id | Distru ID of the connected Dutchie retailer to scope the mapping to. Provide together with dutchie_product_id. |
string | false |
| product_id | ID of the Distru product to map. Must be a product in your company. Required. | string | true |
| treez_photo_url | 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. |
string | false |
| treez_product_id | 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. |
string | false |
| treez_retailer_id | Distru id of the connected Treez retailer to scope the mapping to, as an integer. Provide together with treez_product_id. |
integer | false |
User
A member of your Distru team — the account behind actions like owning or creating records.
| Property | Description | Type | Required |
|---|---|---|---|
| banned | True when this user has been banned and can no longer sign in | boolean | false |
| deleted_at | ISO 8601 datetime this user was deleted at, or null when the user is not deleted | string | false |
| The email address of this user | string | true | |
| full_name | The full name of this user, or null when not set | string | false |
| id | ID for this user | string | true |
| inserted_datetime | ISO 8601 datetime this user was created at | string | true |
| role | A permission role that determines what a user can do in Distru. | Role | false |
UserResponse
A single user envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | A member of your Distru team — the account behind actions like owning or creating records. | User | false |
Users
A collection of Users
| Property | Description | Type | Required |
|---|---|---|---|
| data | Users | array(User) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Vehicle
A vehicle
| Property | Description | Type | Required |
|---|---|---|---|
| color | Color of the vehicle, or null if never set. Always populated for BioTrack-synced vehicles. | string | false |
| description | Free-text name or description for the vehicle, or null if never set. Always populated for BioTrack-synced vehicles. | string | false |
| id | ID of the vehicle. Stable across updates. | string | true |
| inserted_datetime | When the vehicle was created, as a UTC ISO-8601 timestamp. Always present. | string | true |
| license_plate_number | License plate number. Always present. | string | true |
| license_plate_state | 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. | string | false |
| make | Manufacturer of the vehicle. Always present. | string | true |
| model | Model of the vehicle. Always present. | string | true |
| updated_datetime | When the vehicle was last updated, as a UTC ISO-8601 timestamp. Always present. | string | true |
| vin | Vehicle identification number (VIN), or null if never set. Always populated for BioTrack-synced vehicles. | string | false |
| year | Model year as a free-form string (e.g. "2021"), or null if never set. Always populated for BioTrack-synced vehicles. | string | false |
VehicleResponse
A single vehicle envelope
| Property | Description | Type | Required |
|---|---|---|---|
| data | A vehicle | Vehicle | false |
Vehicles
A collection of vehicles
| Property | Description | Type | Required |
|---|---|---|---|
| data | Vehicles | array(Vehicle) | false |
| next_page | URL for the next page of results; null when there is no next page | string | false |
Changelog
2026-08-25
- Added DELETE
/public/v1/strains/{id}. - Added DELETE
/public/v1/vehicles/{id}. - Added DELETE
/public/v1/contacts/{id}. - Added DELETE
/public/v1/custom-fields/{id}. - Added DELETE
/public/v1/companies/{id}. - Added DELETE
/public/v1/products/{id}. - Added DELETE
/public/v1/batches/{id}. - Added DELETE
/public/v1/test-results/{id}. - Added DELETE
/public/v1/purchases/{id}. - Added DELETE
/public/v1/orders/{id}. - Added DELETE
/public/v1/invoices/{id}. - Added DELETE
/public/v1/assemblies/{id}. - POST
/public/v1/packages/{id}now acceptsis_inactiveto inactivate or reactivate a package. - Ability to get and set price tiers on sales order items.
2026-08-24
- Added a
statusfilter to GET/public/v1/assemblies.
New endpoints:
- Added POST
/public/v1/payments/{id}/void.
New response fields:
- compact
packageobject:metrc_id,quantity,quantity_active,location_id, andlicense_id - full
packageobject:product - full
productobject:quantity_active,quantity_active_by_location,quantity_reservedandquantity_available. - full
batchobject:product,quantity_activeandquantity_active_by_location.
New request fields:
- POST
/public/v1/orders:noteon line items;tax_idon charges. - POST
/public/v1/products:treez_wholesale_price,leaflink_product_id. - POST
/public/v1/purchases:supplier_location_id. - POST
/public/v1/custom-fieldsand/public/v1/custom-fields/{id}:disabled_field_options. - POST
/public/v1/companies:qb_customer_id,qb_vendor_id,leaflink_customer_id.
Behavior changes:
- All POST endpoints now support sparse updates.
- Added
include_packages_with_active_quantity_by_locationandinclude_batches_with_active_quantity_by_locationquery params to GET/public/v1/productsand GET/public/v1/products/{id}. When set, each product in the response includes fieldspackages_with_active_quantity_by_locationandbatches_with_active_quantity_by_location.
Cross-cutting filters:
- Added an
idsfilter to all GET list endpoints, restricting the result to specific records by ID. - Added a
custom_datafilter to all applicable GET list endpoints. - Added an
owner_idsfilter to all GET endpoints that were missing it. - Added
inserted_datetimeandupdated_datetime(range) filters to all GET endpoints that were missing them.
Per-endpoint filters:
- GET
/public/v1/assemblies:inserted_datetimelicense_idsowner_idsstatusupdated_datetimeinput_*/output_*variants ofproduct_ids,package_ids,batch_ids,package_compliance_labels,package_batch_numbers,batch_batch_numbers,product_category_ids,product_subcategory_ids,product_group_ids,product_brand_ids,product_vendor_ids,product_strain_ids,product_tag_ids,product_skus
- GET
/public/v1/batches:batch_numbershas_quantity_activeproduct_brand_idsproduct_category_idsproduct_group_idsproduct_idsproduct_skusproduct_strain_idsproduct_subcategory_idsproduct_tag_idsproduct_vendor_ids
- GET
/public/v1/packages:batch_numberbatch_numbersbin_idscompliance_labelcompliance_labelscompliance_product_namecompliance_product_namescontains_remediated_materialexpiration_datetimefinished_datetimeharvest_datehas_coa_attachedhas_quantity_activeinactivated_datetimeis_production_batchis_test_sampleis_trade_samplelab_testing_statelab_testing_stateslicense_idsowner_idspackaged_dateproduct_brand_idsproduct_category_idsproduct_group_idsproduct_skusproduct_strain_idsproduct_subcategory_idsproduct_tag_idsproduct_vendor_idsunit_type_ids
- GET
/public/v1/products:brand_idscategory_idshas_quantity_activeinventory_tracking_methodis_activeis_featuredleaflink_product_idsmenu_idsmenu_visibilitynamesowner_idsproduct_group_idsskuskusstrain_idsstrain_typessubcategory_idstag_idsunit_type_idsupcupcsvendor_ids
- GET
/public/v1/orders:batch_batch_numbersbatch_idsbilling_location_idsbiotrack_idsbuyer_company_idscompany_group_idscompany_idsdelivered_datetimeleaflink_idslocation_idsmenu_idsmetrc_transfer_idsorder_numberorder_numbersowner_idspackage_batch_numberspackage_compliance_labelspackage_idspayment_statusesproduct_brand_idsproduct_category_idsproduct_group_idsproduct_idsproduct_skusproduct_strain_idsproduct_subcategory_idsproduct_tag_idsproduct_vendor_idsshipping_location_idsstatusestotal
- GET
/public/v1/companies:categorycitycompany_group_idshas_outstanding_balanceleaflink_customer_idslegal_business_namelicense_numbernamenamesoutstanding_balanceowner_idsqb_customer_idsqb_vendor_idsrelationship_type_idsstates
- GET
/public/v1/purchases:batch_batch_numbersbatch_idsbiotrack_idscompany_group_idscompany_idslicense_numberlocation_idsmetrc_transfer_idsowner_idspackage_batch_numberspackage_compliance_labelspackage_idspayment_statusesproduct_brand_idsproduct_category_idsproduct_group_idsproduct_idsproduct_skusproduct_strain_idsproduct_subcategory_idsproduct_tag_idsproduct_vendor_idspurchase_numberpurchase_numbersstatusestotal
- GET
/public/v1/invoices:batch_batch_numbersbatch_idscompany_group_idscompany_idsinvoice_numbersis_voidedorder_idsorder_numbersorder_statusesowner_idspackage_batch_numberspackage_compliance_labelspackage_idsproduct_brand_idsproduct_category_idsproduct_group_idsproduct_idsproduct_skusproduct_strain_idsproduct_subcategory_idsproduct_tag_idsproduct_vendor_idsremaining_amountstatusestotalvoided_datetime
- GET
/public/v1/credits:amountcompany_idsinvoice_ids
- GET
/public/v1/payments:amountcompany_idsinvoice_idspayment_method_idspurchase_ids
- GET
/public/v1/returns:company_idsorder_idsstatuses
- GET
/public/v1/contacts:company_ids
- GET
/public/v1/adjustments:batch_idslocation_idsproduct_idsupdated_datetime
- GET
/public/v1/strains:nametypes
- GET
/public/v1/locations:license_numbername
- GET
/public/v1/price-tiers:product_ids
- GET
/public/v1/menus:inserted_datetimeupdated_datetimevisibilities
- GET
/public/v1/test-results:batch_idsinserted_datetimemetrc_idspackage_idsproduct_ids
- GET
/public/v1/inventory:product_brand_idsproduct_category_idsproduct_group_idsproduct_skusproduct_strain_idsproduct_subcategory_idsproduct_tag_idsproduct_vendor_ids
2026-08-21
- POST
/public/v1/creditsnow allows updatingowner_id,external_noteandinternal_noteon automatically-created credits (from a return, an invoice overpayment, or a QuickBooks Online payment), matching what the Distru UI allows on those fields. On a credit memo created in QuickBooks Online,owner_idis the only updatable field. Every other field still requires a manually-created (USERsource) credit.
2026-08-19
- Added POST
/public/v1/assembliesendpoint to create, update, and delete assemblies. - Added POST
/public/v1/assemblies/split_packageendpoint. - Added GET
/public/v1/metrc/tagsand GET/public/v1/metrc/tags/{id}endpoints. - Added GET
/public/v1/metrc/itemsendpoint. - Extended POST
/public/v1/purchasesto acceptstatusand to match a purchase to a compliance transfer - Added
batch_number,status,metrc_item_id,metrc_location_id,metrc_notes,metrc_production_batch_number,copy_custom_data_from_input,use_same_item,is_donation,is_test_sample, andis_trade_sampleto assembly outputs. - Added GET
/public/v1/price-tiers, GET/public/v1/price-tiers/{id}, POST/public/v1/price-tiers, and DELETE/public/v1/price-tiers/{id}endpoints.
2026-08-18
- Added POST
/public/v1/packages/finishendpoint. - Added POST
/public/v1/packages/moveendpoint. - Added
inserted_datetime(the record's creation datetime) to the product, package, user, license, charge, order item and invoice item objects returned across the API.
2026-08-17
- Added GET/POST/DELETE
/public/v1/binsendpoints. - Added
bin_idsto POST/public/v1/batchesand POST/public/v1/packages/:idto set a record's bins. - Added
binsto the response of GET/public/v1/batches, GET/public/v1/batches/:id, POST/public/v1/batches, GET/public/v1/packagesand POST/public/v1/packages/:id, present only when bin inventory tracking is enabled. - Added POST
/public/v1/products/add-costsendpoint. - Added POST
/public/v1/batches/add-costsendpoint. - Added POST
/public/v1/packages/add-costsendpoint.
2026-08-13
- Added GET
/public/v1/taxesendpoint. - Added GET
/public/v1/unit-typesendpoint. - Added GET
/public/v1/official-product-categoriesendpoint. - Added GET/POST/DELETE
/public/v1/product-groupsendpoints. - Added GET/POST/DELETE
/public/v1/product-categoriesendpoints. - Added GET/POST/DELETE
/public/v1/product-subcategoriesendpoints. - Added GET/POST/DELETE
/public/v1/company-groupsendpoints. - Added GET/POST/DELETE
/public/v1/tagsendpoints. - Added GET/POST/DELETE
/public/v1/cost-typesendpoints. - Added GET/POST/DELETE
/public/v1/driversendpoints. - Added GET
/public/v1/creditsendpoint. - Added GET
/public/v1/credits/:idendpoint. - Added POST
/public/v1/creditsendpoint. - Added DELETE
/public/v1/credits/:idendpoint. - Added POST
/public/v1/credits/:id/cancelendpoint. - Added
credit_usesandoverpayment_creditsto the payment object returned by GET/public/v1/paymentsand GET/public/v1/payments/:id. - Added
paymentsto the response of GET/public/v1/purchases, GET/public/v1/purchases/:id, GET/public/v1/invoicesand GET/public/v1/invoices/:id. Each element is the full payment object also returned by GET/public/v1/payments. - On GET
/public/v1/creditsand GET/public/v1/credits/:id, a credit'spayment(the originating invoice payment for overpayment or QuickBooks-linked credits) and eachcredit_usesentry'spaymentare that same full payment object.
2026-08-12
- Added GET
/public/v1/payment-termsendpoint. - Added
default_payment_termto the response of GET/public/v1/companiesand GET/public/v1/companies/:id. - Added
default_payment_term_idto POST/public/v1/companiesfor setting a company relationship's default payment term. - Added
outstanding_balanceto the response of GET/public/v1/companiesand GET/public/v1/companies/:id. - Added 9 PDF download endpoints:
- GET
/public/v1/invoices/:id/pdf— invoice PDF. - GET
/public/v1/invoices/:id/test-results/pdf— combined COA PDF for the invoice. - GET
/public/v1/orders/:id/pdf— sales order slip PDF. - GET
/public/v1/orders/:id/test-results/pdf— combined COA PDF for the order. - GET
/public/v1/purchases/:id/pdf— purchase order PDF. - GET
/public/v1/assemblies/:id/pdf— work order PDF. - GET
/public/v1/test-results/:id/pdf— single test result COA PDF. - GET
/public/v1/batches/:id/primary-test-result/pdf— batch primary test result COA PDF. - GET
/public/v1/packages/:id/primary-test-result/pdf— package primary test result COA PDF.
- GET
- Added
coa_urlto test result objects: a public, non-expiring URL to view or download the test result's COA PDF, ornullwhen no file is attached. Included in GET/public/v1/test-resultsand GET/public/v1/test-results/:id, and in the nestedprimary_test_resultobject of GET/public/v1/batchesand GET/public/v1/packages.
2026-08-03
- Added GET
/public/v1/paymentsand GET/public/v1/payments/:idendpoints. - Renamed
inserted_at→inserted_datetimeandupdated_at→updated_datetime:- GET
/public/v1/returns. - GET
/public/v1/returns/:id. - GET
/public/v1/adjustments. - GET
/public/v1/adjustments/:id. - GET
/public/v1/product-pos-mappings. - GET
/public/v1/product-pos-mappings/:id.
- GET
2026-07-30
- Added
upsert_invoice,email_invoiceandemail_invoice_addressesto POST/public/v1/orders.
2026-07-29
Read/write parity pass across the API — every field that can be set can now be read back, and related resources (owner, locations, notes, custom data, tags) are exposed consistently.
- Added
owner(a full user object) to the response of the following endpoints:- GET
/public/v1/productsand/public/v1/products/:id - GET
/public/v1/purchasesand/public/v1/purchases/:id - GET
/public/v1/companiesand/public/v1/companies/:id - GET
/public/v1/contactsand/public/v1/contacts/:id(previously only the owner's id was returned)
- GET
- Added
billing_locationto the response of POST/public/v1/invoices, GET/public/v1/invoicesand GET/public/v1/invoices/:id. - Added
billing_locationandlocationto the response of POST/public/v1/purchases, GET/public/v1/purchasesand GET/public/v1/purchases/:id. - Added
external_notesandinternal_notesto the response of POST/public/v1/invoices, GET/public/v1/invoicesand GET/public/v1/invoices/:id. - Added
descriptionto the response of GET/public/v1/purchasesand GET/public/v1/purchases/:id. - Added
blaze_payment_typeto the response of GET/public/v1/ordersand GET/public/v1/orders/:id. - Added the following fields to the response of GET
/public/v1/productsand GET/public/v1/products/:id:upcis_featuredwholesale_unit_pricequantity_available_threshold_minquantity_available_threshold_maxtotal_thctotal_cbdtotal_cannabinoid_unittags
- Added
unit_costto the response of GET/public/v1/adjustmentsand GET/public/v1/adjustments/:id. - Added
quickbooks_deposit_account_nameto the response of POST/public/v1/invoices/:id/paymentsand POST/public/v1/purchases/:id/payments. - Added
owner_id,custom_data,external_notesandinternal_notesto POST/public/v1/invoices. - Added
owner_idandcustom_datato POST/public/v1/purchases. - Added
nameandcustom_datato POST/public/v1/batches. - Added
bill_of_materialsto the response of GET/public/v1/products/:id(always included) and GET/public/v1/products(included when theinclude_bill_of_materials=truequery param is set).cost_type.cost_per_unitis only returned to callers with thecosts_permissions_view_cost_types_cost_per_unitpermission. - POST
/public/v1/orders:due_datetimeis now optional. When omitted, the due date is derived from the customer's default payment term, then the company default order payment term, then falls back to the order date (COD).
2026-07-27
- Added GET
/public/v1/reports/sales-order-taxendpoint. - Added GET
/public/v1/reports/inventory-assetsendpoint. - Added GET
/public/v1/reports/sales-order-historyendpoint. - Added GET
/public/v1/reports/sales-order-item-historyendpoint. - Added GET
/public/v1/reports/sales-by-companyendpoint. - Added GET
/public/v1/reports/sales-by-productendpoint. - Added GET
/public/v1/reports/sales-by-userendpoint. - Added GET
/public/v1/reports/order-fulfillmentendpoint. - Added GET
/public/v1/reports/purchase-order-historyendpoint. - Added GET
/public/v1/reports/purchases-by-companyendpoint. - Added GET
/public/v1/reports/purchases-by-productendpoint. - Added GET
/public/v1/reports/invoice-historyendpoint. - Added GET
/public/v1/reports/cogsendpoint. - Added GET
/public/v1/reports/inventory-valuationendpoint. - Added GET
/public/v1/reports/inventory-transaction-historyendpoint. - Added GET
/public/v1/reports/harvest-outputsendpoint. - Added GET
/public/v1/reports/plant-lifecycleendpoint. - Added GET
/public/v1/reports/cultivation-transaction-historyendpoint.
2026-06-29
- Added
gross_weightandgross_weight_unit_typefields to GET and POST/public/v1/products.
2026-06-23
- Added
quickbooks_sync_enqueuedfield to the response of POST/public/v1/invoices/{id}/payments.
2026-06-08
- Added GET/POST
/public/v1/custom-fieldsendpoint - Added GET/POST
/public/v1/vehiclesendpoint - Added GET/POST
/public/v1/strains/:idendpoint - Added
license_numberandinventory_sourceto order responses - Added
custom_datato POST/public/v1/ordersendpoint
2026-05-28
- Added ability to fetch by /id on most endpoints
2026-05-25
- Added GET
/public/v1/returnsendpoint
2026-05-17
- Added
company_idfilter parameter to GET/public/v1/ordersendpoint - Added GET
/public/v1/menusendpoint
2026-05-11
- Added
lab_testing_statefield to GET/public/v1/packagesendpoint - Added support for non-admin users to use the API.
- Added permission checks to most controllers.
2026-05-06
- Added
menu_idandmenu_namefilter parameters to GET/public/v1/productsendpoint
2026-05-01
- Added
custom_datafield to POST/public/v1/productsendpoint
2026-03-27
- Removed
POST /public/v1/products/{id}/imagesendpoint
2026-03-10
- Added
POST /public/v1/products/{id}/imagesendpoint - Added
POST /public/v1/companiesendpoint
2026-02-13
- Added
batch_ids[]query parameter to GET/public/v1/batchesendpoint to filter batches by batch IDs.
2026-02-12
- Added the
completion_datetimeas a query parameter to GETpublic/v1/adjustments - Added the
inserted_atfield topublic/v1/adjustments
2026-02-10
- Added the
payment_terms_namefield to GETpublic/v1/orders
2026-01-23
- Added the
deleted_atfield and thedeletedfilter parameter to the following endpoints:- GET
public/v1/batches(breaking change: deleted batches are no longer returned by default) - GET
public/v1/companies - GET
public/v1/contacts - GET
public/v1/locations - GET
public/v1/payment_methods - GET
public/v1/products(breaking change: deleted products are no longer returned by default) - GET
public/v1/users
- GET
- Added
estimated_departure_datetimeandestimate_arrival_datetimetometrc_transfer_template_transporter_infofield in POSTpublic/v1/orders.- Added automatic estimated departure / arrival calculations for transporters such that, if left blank, the fields will be populated with the first departure estimate being the time that the template was sent to Metrc, the drive time for each transporter being 1 hour, and the departure of the next transporter being the arrival time of the previous transporter.
- Modified Destination estimated departure / arrival times to be the first departure and the last arrival of the specified transporters.
2026-01-21
- Added the
manufactured_datetimefield to GETpublic/v1/batchesand POSTpublic/v1/batches. - Added the
assembly_numberandestimated_start_datefields to GETpublic/v1/assemblies. - Added the
reservedfield to GETpublic/v1/inventory.
2026-01-19
Added the following endpoints:
- POST
public/v1/contacts - POST
public/v1/custom-fields
- POST
Added the following fields to
public/v1/adjustmentscompliance_unit_typeunit_type
Added the
batch_idsandlocation_idsfilter parameters to GETpublic/v1/inventory.Added the
product_idandbatch_numberfilter parameters to GETpublic/v1/batches.Added the
product_namefilter parameter to GETpublic/v1/products.Added the
product_idsfilter parameter to GETpublic/v1/packages.
2025-10-24
Added the following fields to
public/v1/productsunit_net_weight_serving_size_unit_typeunit_net_weightunit_serving_size
2025-10-19
- Added
unit_costto the endpoint GETpublic/v1/products
2025-10-15
Added the following fields to
public/v1/inventorytotal_cost_actualtotal_cost_defaultcost_per_unit_actualcost_per_unit_default
2025-09-10
Added
cost_per_unit_actualandcost_per_unit_defaultto the following endpoints:- GET
public/v1/assemblies - GET
public/v1/orders - GET
public/v1/orders/:id - GET
public/v1/invoices - GET
public/v1/invoices/:id - GET
public/v1/batches - GET
public/v1/packages
- GET
Added fields
total_cost_actual,total_cost_defaultand attributeinclude_coststo the following endpoints:- GET
public/v1/batches - GET
public/v1/packages
- GET
2025-08-21
- Added
groupto GET/public/v1/companies
2025-08-20
- Added GET
/public/v1/product-pos-mappingsendpoint - Added POST
/public/v1/product-pos-mappingsendpoint - Added DELETE
/public/v1/product-pos-mappings/:idendpoint
2025-08-19
- All GET endpoints now return eventually consistent data, with changes taking up to 1 second to propagate in responses
2025-07-01
- Added
description_markdownto GET/POST/public/v1/products
2025-06-12
- Added
metrc_transfer_idandbiotrack_idto GET/public/v1/orders
2025-06-12
- Changed
coston GET/public/v1/assembliestototal_cost_actualiningredients - Added
total_cost_defaultto GET/public/v1/assembliesiningredients - Added
total_cost_actualto GET/public/v1/assembliesinadditional_costs - Added
total_cost_defaultto GET/public/v1/assembliesinadditional_costs
2025-06-09
- Added POST
/public/v1/file-attachmentsendpoint for uploading files and attaching them to business entities (products, orders, purchases, etc.).
2025-05-27
- Added
biotrack_idandmetrc_transfer_idto POST/public/v1/orders
2025-05-22
- Added
external_nameto GET/POST/public/v1/products
2025-05-18
- Added
leaflink_order_numberto GET/public/v1/orders
2025-05-16
- Added GET
/public/v1/test-results - Added POST
/public/v1/test-results
2025-05-15
- Added POST
/public/v1/stock_adjustmentsendpoint.
2025-05-14
- Added
custom_datato the response of the following endpoints:- GET
/public/v1/assemblies - GET
/public/v1/batches - POST
/public/v1/batches - GET
/public/v1/companies - GET
/public/v1/contacts - GET
/public/v1/invoices - GET
/public/v1/invoices/:id - POST
/public/v1/invoices - GET
/public/v1/orders - GET
/public/v1/orders/:id - POST
/public/v1/orders - GET
/public/v1/packages - GET
/public/v1/products - POST
/public/v1/products - GET
/public/v1/purchases - POST
/public/v1/purchases
- GET
2025-05-13
- Added GET
/public/v1/adjustmentsendpoint.
2025-05-06
- Added
is_trade_sampleto GET/public/v1/packagesendpoint.
2025-04-09
- Added the following fields to GET
/public/v1/companiesendpoint:legal_business_namedefault_emailphone_numberinvoice_emailsales_order_emailpurchase_order_emailorder_shipment_emailwebsitedefault_sales_order_notesdefault_purchase_order_notesoutstanding_balance_thresholdowner_id
2025-03-18
- Modified
unit_net_weightandunit_serving_sizein GET/public/v1/productsendpoint: These fields can now be populated regardless of the product's unit type.
2025-03-11
- Added
total_cost_actual,total_cost_defaultandreturned_quantityto "items" in GET/public/v1/invoicesendpoint. - Added
total_cost_actual,total_cost_defaultandreturned_quantityto "items" in GET/public/v1/invoices/:idendpoint. - Added
total_cost_actual,total_cost_defaultandreturned_quantityto "items" in GET/public/v1/ordersendpoint. - Added
total_cost_actual,total_cost_defaultandreturned_quantityto "items" in GET/public/v1/orders/:idendpoint.
2025-03-05
- Added GET
/public/v1/assembliesendpoint.
2025-02-26
- Page size change from 50,000 to 5000 for the following endpoints:
- GET
/public/v1/batches - GET
/public/v1/companies - GET
/public/v1/contacts - GET
/public/v1/inventory - GET
/public/v1/packages - GET
/public/v1/products - GET
/public/v1/purchases
- GET
2025-01-31
- Added POST
/public/v1/batchesendpoint. - Added batch_number, product_id, owner_id and description to GET
/public/v1/batchesendpoint. - Added title and phone_number to GET
/public/v1/contactsendpoint.
2025-01-17
- Added
product_unit_quantityto GET/public/v1/packagesendpoint. - Added
product_unit_typeto GET/public/v1/packagesendpoint.
2025-01-15
- Added POST
/public/v1/purchases/:id/paymentsendpoint. - Added POST
/public/v1/invoices/:id/paymentsendpoint.
2025-01-08
- Added GET
/public/v1/payment-methodsendpoint.
2024-12-26
- Added msrp, is_active, category.type and images.rank to GET
/public/v1/products.
2024-12-24
- Added billing_location_id to POST
/public/v1/purchasesendpoint.
2024-11-06
- Field thc of type string in the Product object was replaced with total_thc field of type number.
- Field cbd of type string in the Product object was replaced with total_cbd field of type number.
- Added total_cannabinoid_unit field to Product object. Allowed values are either "MG" or "PERCENT".
2024-10-02
- Added GET
/public/v1/strainsendpoint. - In endpoint GET /public/v1/companies, added fields category and relationship to the response.