SeedLink
CustomersSign inRequest access

Guides/METRC field guides for Rhode Island

The Rhode Island METRC API returns an empty HTTP 200 when the required date filters are missing, and several other traps produce the same empty answer

On Rhode Island production, GET /packages/v2/active answers HTTP 200 and an empty list when lastModifiedStart and lastModifiedEnd are missing, and GET /items/v2/active does the same. Nothing in the response says a parameter was required. SeedLink’s sync sends a 365-day window on every call; on 2026-08-20 that call returned 258 packages from one sandbox facility in about 105 seconds, after the route that started it had already answered at a 30-second timeout with a count of 0 synced. Each of the other traps on this page produces the same kind of answer: a response that looks healthy and is not.

Observed
August 20, 2026
Revised
September 20, 2026
Environment
Rhode Island production and sandbox

What this page is

This page records what SeedLink’s integration observed on the Rhode Island METRC API, dated, with the call that produced each observation. It is a catalog of the answers that look healthy and are not: an empty HTTP 200, a refused page that a loop reads as the last page, a sync that reports zero because its route timed out, a read-only key whose reads are fine. It is not METRC documentation and it is not regulatory guidance.

Two behaviours here are recorded in SeedLink’s field guide without a date: the empty answer when the date filters are missing, and the proportion of rate-limit responses that carry Retry-After: 0. They are stated as the rules the production sync was written around; every other observation carries the day it was made. Sandbox observations are marked as sandbox, and production does not share all of them. Other states run their own METRC instances and can differ on every point below.

How every number on this site is observed, dated and bounded is on the methodology page, which also lists what SeedLink does not publish.

An empty HTTP 200 is not an empty facility

lastModifiedStart and lastModifiedEnd are required on GET /packages/v2/active and GET /items/v2/active. Leave them off and Rhode Island production answers HTTP 200 with an empty list and no message that a parameter was missing. An integration that reads that answer as inventory concludes that the facility holds nothing, and the same answer comes back on every retry.

SeedLink’s sync sends a 365-day window on every call, and Rhode Island production accepted a window a year wide when that default was tested. The cap is not uniform across facilities: a California sandbox and the Rhode Island dedicated sandbox both enforce a window of at most 24 hours and return HTTP 400 for anything wider. SeedLink’s sync sends the wide window first and, on the specific 400 that means the window is too large, chunks the span into 24-hour slices and dedupes, so one code path serves a facility with the cap and one without. Do not assume the window a production facility accepts is the window its sandbox accepts.

The window also has to be in the unit the helper takes. SeedLink’s manifest code once passed a number of days to a helper that took seconds, so a request meant to cover a year asked for the last 365 seconds, about 6 minutes, and pulled an empty list.

The route that starts a sync is a third source of a false zero: the 2026-08-20 sync in the lede finished about 105 seconds in, long after its route had reported 0 synced at a 30-second timeout, and nothing was wrong with METRC or the request. A count of zero from a caller that stopped waiting is not a count.

What was asked, what Rhode Island answered, and what it meant
What you askedWhat RI answeredWhat it meant
GET /packages/v2/active with licenseNumber and no date filtersHTTP 200, an empty listThe filters are required; not an empty facility (production; undated in the record)
GET /items/v2/active with no date filtersHTTP 200, an empty listThe same rule; items need the window too
lastModifiedStart 365 seconds back (days passed to a helper that took seconds)HTTP 200, an empty listA window of about 6 minutes; nothing had been modified in it
A 365-day windowHTTP 200 and the facility’s packages, page by pageA correct call: 258 packages in about 105 seconds on 2026-08-20
A sync started by a route with a 30-second timeoutThe route reported 0 syncedThe sync was still running and finished after the route had answered (2026-08-20)
A window wider than 24 hours, California sandboxHTTP 400That sandbox enforces the window width; Rhode Island production accepted a year
GET /transfers/v2/rejected for one cultivator, 2026-07-25HTTP 200 with 0 rowsA real endpoint answering with an empty list for that cultivator on that day

SeedLink’s sync treats an empty package list as a reason to stop rather than a result to apply: it skips its deactivation pass, keeps every catalog row active and marks the run as skipped for staleness. The guard exists because a silent empty answer once zeroed out an entire catalog, and a transient outage, a sandbox reset or a rotated license produces the same empty list as a missing filter.

pageSize caps at 20, and a 400 is not an empty page

Every paginated Rhode Island endpoint refuses a pageSize above 20 outright. The answer is HTTP 400 with the body pageSize must be a positive number between 1 and 20., observed on GET /transfers/v2/deliveries/{id}/packages; the cap is server-side and applies to /packages/v2/* and /transfers/v2/* alike.

The trap is a loop that treats any status other than 200 as the end of the data. It turns the 400 into an empty array and reports zero packages with no sign that anything went wrong, which looks identical to a genuinely empty manifest. That cost one read-only probe a wrong answer during a transfer reconciliation in June 2026: the manifest had packages, the probe asked for a page larger than 20, and the loop read the refusal as the last page.

PageSize in a Rhode Island response is the row count of the current page, not the size requested: a 42-row result paged at 20 reports PageSize 20, 20 and then 2, so it must not be held constant or used to recompute TotalPages. A completeness claim follows TotalPages and Total (or TotalRecords) and checks that the rows counted agree with the total; a short page alone cannot prove that the requested page size was honoured.

Pagination and rate-limit rules on the Rhode Island API
RuleWhat Rhode Island doesWhat to do
pageSize above 20HTTP 400, pageSize must be a positive number between 1 and 20.Pin pageSize to 20 and throw on any status other than 200 instead of ending the loop
PageSize in the responseThe row count of the current page: 20, 20, then 2 for 42 rowsFollow TotalPages and Total or TotalRecords; never recompute pages from PageSize
HTTP 429 on GET /labtests/v2/results during a syncRetry-After: 0 on most of them; a few carry values in the fifties (one development environment’s log, undated)Floor the honoured delay at your own exponential backoff, at least 3.5 seconds across three sleeps
Transient failures: a 500-class status, 408, 425Arrive from the same load balancer as the successesRetry on idempotent methods only; one of these used to be fatal while a dropped connection was retried
Concurrency against one facilityMETRC rate-limits per facility, so one sync can burst against its own licenseA per-credential gate: 4 requests in flight, at least 60 ms apart
Sandbox rate limitsA 23-second Retry-After after two full syncs and a packaging scan inside a few minutes (July 2026)Pace the heavy steps on the sandbox; read the bail as pacing, not as a defect

Retry-After: 0 arrives in bulk, and honouring it is a hot loop

GET /labtests/v2/results answers HTTP 429 over and over during a sync, and on Rhode Island the Retry-After header on those responses is almost always 0. One development environment’s log held hundreds of rate-limited rows carrying Retry-After: 0 against a few dozen carrying values in the fifties, still arriving daily when the count was made; the record does not date the count.

A client that honours the header literally sleeps for zero and retries at once, which can never de-correlate from the burst that caused the limit. SeedLink’s client capped the ceiling of the delay but had no floor, so all three attempts burned in well under a second and the fetch was dropped. The fix floors the honoured delay at the client’s own exponential backoff, at least 3.5 seconds across the three sleeps, so a zero degrades to a real wait.

The damage from a dropped lab fetch is worse than a blank cell. On Rhode Island Item.UnitThcPercent is null and the potency numbers exist only in /labtests/v2/results, so a swallowed fetch writes null over a previously good potency; and because results are fetched per item and fanned out to sibling packages, one dropped request removes the potency from every package of that item at once.

The transient-failure retry, the per-credential gate and the sandbox’s 23-second Retry-After are in the table above. They exist for one reason: METRC rate-limits per facility, so the hazard is one sync bursting against its own license.

What 401, 400 and 404 mean: the key, the facility and the path

Authentication is HTTP Basic with base64(VENDOR_KEY:USER_KEY): the vendor key belongs to the integrator and is issued per state and per environment, the user key belongs to the cultivator. Sandbox and production are separate hosts, sandbox-api-{state}.metrc.com and api-{state}.metrc.com, with separate vendor keys, and which one a call reaches is a property of the credential, not of the deployment. The one exception to Basic is the sandbox integrator setup, POST /sandbox/v2/integrator/setup, which authenticates with an x-metrc-key: <vendor key> header; its response shape is undocumented.

On 2026-06-04 a vendor key issued as read-only made every GET on Rhode Island production answer HTTP 200 and every POST and PUT answer HTTP 401. The sync log stayed green the whole time, because reads were healthy. The tell is two different write endpoints both answering 401: that is the key’s scope, where a payload problem answers 400.

A cultivator credential cannot read retail sales: in the sweep of 2026-07-25 every /sales/* and /patients/* path answered HTTP 401.

An Item belongs to the facility that created it. GET /items/v2/{id} takes licenseNumber as optional and returns 200 across facilities. PUT /items/v2/ requires licenseNumber and only the owning facility accepts it: a non-owning facility answers 401, 404, or HTTP 400 with the body Item N does not exist at the current Facility., and all three mean try the owner. The 400 shape was observed on the Rhode Island sandbox on 2026-07-09; a walk across /facilities/v2/ that retried only on 401 and 404 stopped at the 400 and never reached the owner.

Two 404s from the same sweep are path mistakes rather than missing data, both in the table below: a plural where the path is singular, and a per-package history endpoint that does not exist. SeedLink calls about 25 of METRC’s 268 documented paths; the 49-endpoint sweep on 2026-07-25 sorted the paths that exist from the ones that do not.

What a 401, 400 or 404 meant on the Rhode Island API, by where it was seen
StatusWhereWhat it meant
401 on every POST and PUT, 200 on every GETRhode Island production, 2026-06-04A vendor key issued as read-only; the payload was never examined
401 on /sales/* and /patients/*Rhode Island production, 2026-07-25Credential scope: a cultivator license cannot read retail sales or patients
401, 404 or 400 Item N does not exist at the current Facility. on PUT /items/v2/Rhode Island sandbox, 2026-07-09Wrong facility for that item; try the owning license
400 pageSize must be a positive number between 1 and 20.Rhode Island production, June 2026The page size, not the data; the page was never fetched
404 on GET /tags/v2/packages/availableRhode Island production, 2026-07-25A plural in the path; the real path is singular
404 on GET /packages/v2/{id}/historyRhode Island production, 2026-07-25No such endpoint; there is no per-package audit trail

The sandbox is not production: adjustments echo and quantities go negative

On 2026-07-23 the Rhode Island sandbox answered a package adjustment in a way production does not. After PUT /packages/v2/adjust, GET /packages/v2/{label} reported the last adjustment amount as the package’s Quantity rather than the running balance: an adjustment of -9 on a 9 g package read back as -9, and a later adjustment of +4.5 read back as +4.5. Repeated reads returned the same value.

PUT /packages/v2/finish then validated against that echoed value, so the condition that a package be empty before it is finished could not be satisfied. Negative package quantities are real on the sandbox. SeedLink has not read a production package back after an adjustment, so this page makes no claim either way about production; a sandbox result of this shape is not evidence about a production package.

The sandbox is also slower to forgive: the 23-second Retry-After above came from it, and a full sandbox sync of 186 packages took about 36 to 38 seconds on 2026-07-23.

The package shape: Item is inline, Quantity is a decimal, LastModified moves on every change

GET /packages/v2/active returns the item inline on each package, as a nested Item object carrying Id, Name, StrainName and ProductCategoryName. There is no top-level ItemId on the package; a mapper that looks one up finds nothing and, depending on how it was written, drops the row or the field.

Quantity is a decimal for weight-based packages, for example 1320.46 grams, so an integer column takes the floor of it and the real weight has to be kept elsewhere. LastModified moves on every change whatever its source, a person in METRC Connect or an integration, and METRC’s own integration guidance is to poll from oldest to newest with a 5-minute buffer behind the newest timestamp, to absorb clock drift between the caller and METRC.

Check it yourself with two read-only calls

The headline question is answerable with two GETs and no writes: the same call without and with the required window.

GET https://api-ri.metrc.com/packages/v2/active?licenseNumber=<your license>
Authorization: Basic <base64 of VENDOR_KEY:USER_KEY>

GET https://api-ri.metrc.com/packages/v2/active?licenseNumber=<your license>
    &lastModifiedStart=<ISO timestamp, 365 days back>
    &lastModifiedEnd=<ISO timestamp, now>
    &pageSize=20&pageNumber=1
Authorization: Basic <base64 of VENDOR_KEY:USER_KEY>

compare the two answers: read Total (or TotalRecords) and TotalPages from each
walk the second answer by pageNumber up to TotalPages and count the rows
stop on any status other than 200 and print it; never treat it as the last page

If the first answer is empty and the second has pages, the filters were the difference. If both are empty, look at the credential and the host before the facility: a read-only key still reads, a sandbox key reads a sandbox, and a route that gave up at its timeout reports whatever it had when it stopped.

Questions

Why does GET /packages/v2/active return an empty list on Rhode Island?
Because lastModifiedStart and lastModifiedEnd are required and Rhode Island production answers HTTP 200 with an empty list when they are missing, with no error. Send a window; SeedLink’s default is 365 days, and production accepted a year.
How wide can the lastModified window be?
Rhode Island production accepted a window a year wide when SeedLink’s 365-day default was tested. A California sandbox enforces at most 24 hours and answers HTTP 400 for anything wider.
My paginator says a manifest has zero packages, but METRC Connect shows some. Why?
Check the page size first. Any pageSize above 20 is refused with HTTP 400 and pageSize must be a positive number between 1 and 20., and a loop that treats a status other than 200 as the end of the data reads that refusal as an empty last page. Pin pageSize to 20, throw on any other status, and follow TotalPages.
Should I honour Retry-After: 0?
Not literally. On Rhode Island most rate-limited responses from GET /labtests/v2/results carry Retry-After: 0, and sleeping for zero is an immediate retry into the same burst. Floor the delay at your own exponential backoff, at least 3.5 seconds across three sleeps.
Every write returns 401 but every read works. What is wrong?
The key, not the payload. On 2026-06-04 a vendor key issued as read-only did exactly that on Rhode Island production, and the sync log stayed green because reads were healthy. Two different write endpoints both answering 401 is scope; a payload problem answers 400.
PUT /items/v2/ says the item does not exist, but GET finds it. Why?
The item belongs to another facility. GET /items/v2/{id} reads across facilities, while PUT /items/v2/ is accepted only by the owner, and a non-owning license answers 401, 404, or 400 with Item N does not exist at the current Facility. (Rhode Island sandbox, 2026-07-09). Try the owning license.
After an adjustment, the sandbox reads the adjustment amount as the package quantity. Is production the same?
Unknown, and this page does not answer it. On 2026-07-23 the Rhode Island sandbox reported the last adjustment amount as Quantity (an adjustment of -9 on a 9 g package read -9) and finish validated against it. SeedLink has not read a production package back after an adjustment, so it has no observation of production either way. Treat the echo as a sandbox behaviour and verify on the facility you are actually calling.
The sync said 0 synced and the catalog filled in a minute later. Which is right?
The catalog. On 2026-08-20 a sync of 258 packages finished in about 105 seconds after its route had returned at a 30-second timeout reporting 0 synced. A count from a caller that stopped waiting is not a count.

Sources and how to cite this page

  • SeedLink METRC field guide (docs/METRC.md), sections Auth & Credentials, Sync (with the pageSize cap and the Retry-After: 0 notes) and Items Are Facility-Scoped, read 2026-09-20. The empty answer without date filters and the Retry-After counts are recorded there without a date.
  • Read-only sweep of 49 METRC endpoints at one Rhode Island production cultivator facility, 2026-07-25.
  • A vendor key issued as read-only: every GET 200, every POST and PUT 401, Rhode Island production, 2026-06-04.
  • Rhode Island sandbox: adjustment echo reads and a full sync of 186 packages, 2026-07-23; a 23-second Retry-After after two syncs and a packaging scan, July 2026 (month only).
  • Sync of 258 packages timed against its route’s 30-second timeout, Rhode Island sandbox, 2026-08-20.

SeedLink, "The Rhode Island METRC API returns an empty HTTP 200 when the required date filters are missing, and several other traps produce the same empty answer", observed 2026-08-20, revised 2026-09-20, https://www.seedlink.net/guides/metrc/rhode-island-api-traps

Every figure above carries the date it was observed and the environment it was observed in. A count belongs to one facility on one day; the field names, orderings and error messages are the durable part.

Machine-readable copy of this page, with the same facts, dates and sources: /guides/metrc/rhode-island-api-traps/data.json

Related

  • A METRC transfer template becomes a manifest only when a person dispatches it in METRC Connect: Rhode Island exposes no dispatch endpoint
  • METRC package adjustments in Rhode Island: Quantity is the new total, not a delta, and the adjustments log reports the difference
  • METRC package tags in Rhode Island: adult-use and medical share one list, and medical comes first
  • METRC lab results in Rhode Island follow a package that was created before the results came back
  • All metrc field guides for rhode island
  • Methodology: what SeedLink publishes and what it does not
SeedLink
GuidesMethodologyContactPrivacyTermsFor Rhode Island buyers
© 2026 SeedLink