API

Incremental Sync

Load everything once

curl --location 'https://app.distru.com/public/v1/orders' \
--header 'Authorization: Bearer ********* API KEY HERE *********'

Every run after that: what changed since your previous run started, minus 5 minutes

curl --location 'https://app.distru.com/public/v1/orders?updated_datetime=2025-05-04T04:35:00.000000Z,' \
--header 'Authorization: Bearer ********* API KEY HERE *********'

And what was deleted since the same time

curl --location 'https://app.distru.com/public/v1/deleted-records?record_type=ORDER&deleted_datetime=2025-05-04T04:35:00.000000Z,' \
--header 'Authorization: Bearer ********* API KEY HERE *********'

To keep a copy of your Distru data in another system, load it once and then fetch only what changed. We recommend every integration that keeps a copy to work this way: the paginated list endpoints with an updated_datetime filter are rate limited, and only a call whose updated_datetime lower bound is within the last 30 days is free (see List rate limits). Looking records up in your copy, rather than with those list calls, keeps lookups out of your rate-limit budget.

Lists that aren't paginated or have no updated_datetime filter, such as inventory levels, licenses, payment methods and most Metrc lists, are not rate limited: reload them in full as often as you need.

  1. Load everything once. Walk the list endpoint with no updated_datetime filter, following next_page until it is null. The time of its first response is your starting point. Take every time from the Date header of Distru's responses, not your own clock.
  2. Then fetch only changes. Request the list with updated_datetime=<starting point minus 5 minutes>,, walk every page, and upsert each record into your copy by id. Note the time of the run's first response. Keep the 5-minute overlap: it catches changes that were still being saved when your previous run started.
  3. Avoid other filters. Filter by status, location, ids or anything else in your copy, not on sync requests. A record that stops matching another filter would never come back, and it isn't deleted, so it would stay stale in your copy. Options that only shape the response, such as include_costs on packages, are fine.
  4. Prefer an unrestricted key. An API key whose user has no team restrictions sees every record its permissions allow. Under team restrictions, a record that moves out of the user's teams stops appearing without being deleted, and one that moves in can be older than your starting point.
  5. Remove deleted records. In the same run, walk GET /public/v1/deleted-records once per record type you sync with the same lower bound in deleted_datetime, and delete those ids from your copy. It lists every deleted record of every type with its own list or get-by-id endpoint (the Metrc lists, which mirror Metrc, aside), kept for 90 days; if your starting point is older than that, reconcile (step 7) before your next run.
  6. Move your starting point only on success. A run's time becomes the new starting point only after it walked every page. If a run fails, the next one starts from the same point.
  7. Reconcile once a week. Load each list in full, noting the time of its first response. Upsert every record it returns, then remove the ones your copy has that it didn't return and that no run upserted since the reload started (a record created during a long reload can land behind its cursor). The reload sends the same parameters as your runs. Keep your runs going as normal; when the reload finishes, set your starting point back to the reload's time, so the next run fetches again whatever changed while it ran. This picks up the changes updated_datetime does not reflect (below) and anything a run missed. Avoid reloading a rate-limited list in full more often than that: every page of a full reload counts against the rate limit.

What moves updated_datetime

On orders, invoices, packages and assemblies, updated_datetime moves whenever the record's own data changes, nested data included (packages offer it as a filter only, not as a field):

  • Orders: the order's own fields, its line items and charges, and the invoices and returns shown on it (including each line's returned quantity).
  • Invoices: the invoice's own fields, its line items and charges (including the order line details shown on each), its payments, credit uses and overpayment credits, and its paid and remaining amounts.
  • Packages: the package's own fields, the bins it is in, its primary test result, its owner, its compliance fields, and its actual cost (total_cost_actual and cost_per_unit_actual with include_costs).
  • Assemblies: the assembly's own fields, its inputs, outputs, costs and bins, its inputs' and outputs' actual costs (total_cost_actual and cost_per_unit), and its outputs' compliance fields.

On other types, nested data can change without moving updated_datetime; the weekly reconcile picks those changes up. On every type, it does not move for:

  • Changes to other records the record points at, for example the name of a product, location, bin or company, or the current quantity of a package used in an assembly. Sync those records from their own endpoints.
  • The order shown on an invoice, or the combined order shown on a merged order. Sync orders from GET /public/v1/orders.
  • Values Distru computes from other records, such as a product's quantities, a company's balances, or the default costs of packages and assemblies. For stock levels, use GET /public/v1/inventory, which is not rate limited.
  • Tasks linked to the record, and adding, renaming or deleting a custom field definition.
  • Deleting a record: see step 5.

List rate limits

A list call over the limit

HTTP/1.1 429 Too Many Requests
retry-after: 12
x-ratelimit-limit: 3000
x-ratelimit-remaining: 0
x-ratelimit-reset: 36000
content-type: application/json
{
  "errors": [
    {
      "message": "List rate limit exceeded: 3000 calls per account, refilling at 5 per minute. Calls with an updated_datetime lower bound within the last 30 days are not limited. Retry after the Retry-After period.",
      "pointer": ["base"]
    }
  ]
}

These rules apply to the paginated list endpoints with an updated_datetime filter:

Getting a single record by id is not limited. Packages have no get-by-id endpoint: look them up in your copy if you keep one; otherwise list them by ids or label (each call costs one).

  • Your account has one budget of 3,000 calls, shared by all these endpoints and all your API keys. It refills at 5 calls per minute (300 per hour), up to 3,000.
  • Each page costs one call: the first page and every next_page alike.
  • A call whose updated_datetime lower bound is within the last 30 days is free: it costs nothing and is never refused. Every other call costs one, including lookups by ids, number or label, calls with only an upper bound, and calls rejected with a 400 or 403.
  • Responses from these endpoints carry your budget: X-RateLimit-Limit (3,000), X-RateLimit-Remaining (calls left) and X-RateLimit-Reset (seconds until the budget is full again).
  • Past the budget, a call returns 429 Too Many Requests with a Retry-After header: the seconds until your next call is available. Wait that long, then retry the same URL. A first load longer than the budget carries on at the refill rate, resuming from the same next_page.

Pagination

First page: request the endpoint with no page parameter

curl --location 'https://app.distru.com/public/v1/products' \
--header 'Authorization: Bearer ********* API KEY HERE *********'

Next page URL in the response body (null when there is no next page)

"next_page": "https://app.distru.com/public/v1/products?page[after]=<opaque cursor>"

Following it: pass the URL back exactly as given

curl --location --globoff 'https://app.distru.com/public/v1/products?page[after]=<opaque cursor>' \
--header 'Authorization: Bearer ********* API KEY HERE *********'

Request a list endpoint with no page parameter to get the first page. Paginated responses carry a next_page URL; call it as given — it already carries the filters you sent — and repeat until next_page is null. That is the whole pagination contract.

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 walk a list by following next_page.

Filtering by datetime parameters

On or after May 4th, 2025

products?updated_datetime=2025-05-04T04:40:21.817570Z,

On or before May 4th, 2025

products?updated_datetime=,2025-05-04T04:40:21.817570Z

Between May 4th and Sept 18th, 2025 (inclusive)

products?updated_datetime=2025-05-04T04:40:21.817570Z,2025-09-18T16:27:44.946871Z

Datetimes use the format YYYY-MM-DDTHH:MM:SS.MSZ.

The comma position controls the direction of the filter. All bounds are inclusive: a record matching the exact datetime in the query is returned.

  • Comma after the datetime = records on or after that datetime.
  • Comma before the datetime = records on or before that datetime.
  • Datetimes on both sides of the comma = records between the two datetimes, inclusive of both ends.

On this page