NAV Hide code

Need more from the API?

We're actively expanding the API and would love to build what your integration needs. Tell your Customer Support Representative which endpoints or functionality would help—we're eager to add them.

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.

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

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:

  1. Log in to Distru with your admin account.
  2. Navigate to the Settings page from the left menu.
  3. Click on Distru API under the Integrations section.
  4. 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-After header (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:

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 (object is the full order, same as GET /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 (object is 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-signature header

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:

  1. Read the raw request body exactly as received — do not re-serialize the JSON, since key order must match.
  2. Compute HMAC-SHA256(raw_body, your_secret) and hex-encode it.
  3. Compare sha256=<your digest> to the x-distru-signature header 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.

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:

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:

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 — built by hand in Distru or via the upsert endpoint (the only source this API can modify).
  • SALES_ORDER — generated to fulfill a sales order.
  • SPLIT_PACKAGE — produced by splitting a Metrc package (including via the split-package endpoint).
  • LAB_TESTING — created to pull a test sample. Note: this attribute is unreliable for assemblies created before September 2020, which may report MANUALLY_CREATED regardless of their true origin.

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 — still in progress; ingredient inventory is claimed but not yet consumed and the assembly can still be edited or deleted.
  • COMPLETED — finished; inputs are consumed and outputs produced into inventory, and the assembly is frozen.

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 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:

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

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:

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

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:

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

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
email 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: PRODUCTproduct_id, LOCATIONlocation_id, BATCH_NUMBERbatch_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 — no payments recorded yet
  • PARTIALLY_PAID — some, but not all, of the total has been paid
  • FULLY_PAID — paid in full
  • OVER_PAID — recorded payments exceed the total. Rare and effectively legacy: new overpayments are converted into a customer credit instead of moving an invoice into this status.

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

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 — nothing has been paid.
  • PARTIALLY_PAID — some but not the full amount has been paid.
  • FULLY_PAID — paid in full.
  • OVER_PAID — paid more than the order total.
At most 200 values.
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:
  • ACTIVE — active in both Distru and the compliance system
  • ASSEMBLING — fully consumed by a pending assembly
  • DESTROYED — destroyed (BioTrack only; Metrc packages never reach this)
  • DISCONTINUED — discontinued in Metrc
  • FINISHED — finished (quantity 0)
  • ONHOLD — on hold in Metrc (Metrc only)
  • RETURNING — tied to a Metrc return that has shipped
  • SELLING — assigned to a Distru sales order
  • SOLD — sold on a Metrc-enabled sales order
  • TRANSFERRED — transferred out of its Metrc license
Note: the status field in responses is returned lowercase (e.g. active); this filter accepts either case. (The old statuses filter name still works but is deprecated.)
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. • lockedtrue 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: Inventory is defined by individual packages (the tracking method used for state-compliance/Metrc packages).
  • PRODUCT: Not grouped in any manner. Inventory is a simple running quantity you add to or remove from as you transact.
  • BATCH: Grouped into batches that share common traits such as expiration dates and test results.

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:
  • DO_NOT_INCLUDE: The product appears on no menus; any menus you send are ignored.
  • INCLUDE_IN_ALL: The product appears on every menu in your company; menus is ignored.
  • INCLUDE_IN_SELECT: The product appears only on the menus listed in menus, which fully replaces its current menu set. menus is required in this mode.
Required whenever 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 — nothing has been paid.
  • PARTIALLY_PAID — some but not the full amount has been paid.
  • FULLY_PAID — paid in full.
  • OVER_PAID — paid more than the purchase total.
At most 200 values.
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[]=&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[]=&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[]=&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[]=&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[]=&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[]=&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[]=&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_purchaseslast_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[]=&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: created by a user in Distru or via the API.
  • SALES_ORDER: created automatically to repackage inventory while fulfilling a sales order.
  • SPLIT_PACKAGE: created by splitting an existing package into smaller packages.
  • LAB_TESTING: created to pull a test sample for lab testing.

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: still in progress — its ingredient inventory is already claimed (each ingredient's active quantity is decreased to hold it for this assembly), but its output products have not been produced into inventory yet.
  • COMPLETED: the assembly has been finished, consuming its claimed ingredient inventory and creating its output products; a completed assembly can no longer be deleted and only a limited set of its fields can be edited.

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).
  • CHARGE: adds to the total (e.g. a delivery fee). Tax lines are also returned as CHARGE, with the tax object populated.
  • DISCOUNT: subtracts from the total.

DISCOUNT CHARGE
string true
unit_type How this line's amount is expressed (SCREAMING_CASE).
  • PRICE: a flat money amount; percent is null.
  • PERCENT: a percentage; the rate is carried in percent.

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.
  • NOT_PAID: no payments applied.
  • PARTIALLY_PAID: paid in part but less than the total.
  • FULLY_PAID: paid in full.
  • OVER_PAID: payments exceed the total.
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
email 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: has a remaining balance still available to apply.
  • REDEEMED: fully applied, remaining_balance is "0.00".
  • CANCELED: voided; its balance can no longer be applied.

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
email 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
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

A single menu wrapped in a data envelope

Property Description Type Required
data Menu false

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: the template is queued to be created or updated in Metrc.
  • COMPLETED: the template exists in Metrc and matches this order.
  • FAILED: the last sync attempt failed — see metrc_transfer_template_error for the reason.

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: does not affect inventory — assigning packages/batches to line items does not change their active quantity, unfulfilled items do not add to the product's reserved quantity, and the order cannot be associated with a compliance transfer.
  • PROCESSING: affects inventory — assigning a package/batch to a line item moves that quantity out of active and into a committed selling state, unfulfilled items add to the product's reserved quantity, and the order may be associated with a compliance transfer.
  • READY_TO_SHIP: same inventory behavior as PROCESSING, but every line item must be fulfilled.
  • DELIVERING, DELIVERED, and COMPLETED: every line item must be fulfilled, and the order must be associated with a compliance transfer if it contains any package-tracked items.
  • CANCELED: the order has been canceled — like PENDING, it does not affect inventory and cannot be associated with a compliance transfer.

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: on hand and available.
  • ASSEMBLING: allocated to a pending assembly.
  • SELLING: reserved on an open sales order.
  • SOLD: consumed by a completed sale.
  • RETURNING: on an in-progress return.
  • TRANSFERRED: sent out on a compliance transfer.
  • ONHOLD: placed on hold in the compliance system.
  • FINISHED: finished in the compliance system.
  • DISCONTINUED: discontinued.
  • DESTROYED: destroyed.

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: on hand and available.
  • ASSEMBLING: allocated to a pending assembly.
  • SELLING: reserved on an open sales order.
  • SOLD: consumed by a completed sale.
  • RETURNING: on an in-progress return.
  • TRANSFERRED: sent out on a compliance transfer.
  • ONHOLD: placed on hold in the compliance system.
  • FINISHED: finished in the compliance system.
  • DISCONTINUED: discontinued.
  • DESTROYED: destroyed.

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_activequantity_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: no active payments recorded.
  • PARTIALLY_PAID: payments cover part of the total.
  • FULLY_PAID: payments equal the total.
  • OVER_PAID: payments exceed the total. May be null for orders that have never had their payment status computed.

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.
  • PENDING, PROCESSING, and DELIVERING behave identically: the order has not been received and does not affect inventory.
  • PARTIALLY_RECEIVED: works together with each line item's received_quantity — when at least one item has a positive received_quantity but not every item has received_quantity equal to its quantity, the order must be in this status, and the received amounts are brought into inventory. Not supported for orders that contain package-tracked items.
  • COMPLETED: the whole order has been received, bringing its inventory into your facility; if the order has package-tracked items it must be associated with a compliance transfer.

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: Distru searches for the best applicable tier on every save of the order. The moment one matches, the line locks to that tier's current version — the stored mode becomes OVERRIDE and price_tier_version is set — so an item created as AUTO reads back as OVERRIDE once a tier has applied. It stays AUTO (with a null price_tier_version) only while no tier matches.
  • OVERRIDE: the line is locked to the exact tier snapshot in price_tier_version. Its discount is re-applied to price_base on every save, and later edits to the live tier do not change this line's pricing.
  • NONE: price tiers are disabled for this line; price equals price_base and price_tier_version is always null.

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

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:

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
email 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

2026-08-24

New endpoints:

New response fields:

New request fields:

Behavior changes:

Cross-cutting filters:

Per-endpoint filters:

2026-08-21

2026-08-19

2026-08-18

2026-08-17

2026-08-13

2026-08-12

2026-08-03

2026-07-30

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.

2026-07-27

2026-06-29

2026-06-23

2026-06-08

2026-05-28

2026-05-25

2026-05-17

2026-05-11

2026-05-06

2026-05-01

2026-03-27

2026-03-10

2026-02-13

2026-02-12

2026-02-10

2026-01-23

2026-01-21

2026-01-19

2025-10-24

2025-10-19

2025-10-15

2025-09-10

2025-08-21

2025-08-20

2025-08-19

2025-07-01

2025-06-12

2025-06-12

2025-06-09

2025-05-27

2025-05-22

2025-05-18

2025-05-16

2025-05-15

2025-05-14

2025-05-13

2025-05-06

2025-04-09

2025-03-18

2025-03-11

2025-03-05

2025-02-26

2025-01-31

2025-01-17

2025-01-15

2025-01-08

2024-12-26

2024-12-24

2024-11-06

2024-10-02