GuidesMETRC field guides for Rhode Island
METRC serves the lab COA PDF for free in Rhode Island: one GET returns the raw PDF, and whether a document exists is a per-lab fact
The Rhode Island METRC API hands back the lab’s own COA PDF with no lab portal in the path. Each row of GET /labtests/v2/results carries a LabTestResultDocumentFileId, and GET /labtests/v2/labtestdocument/{id}?licenseNumber= returns that document as raw application/pdf bytes: one live document read on 2026-07-25 at a Rhode Island production cultivator facility was 327,580 bytes and began with %PDF-. The id is populated by the lab, not by METRC, so whether a document exists at all is a per-lab fact. Across the three production cultivators SeedLink syncs, packages tested by one lab carried a document on 12 of 12 (100%) and packages tested by a second lab on 0 of 36 (0%), read the same day.
- Observed
- Revised
- Environment
- Rhode Island production and sandbox
What this page is
This page records what SeedLink’s production integration observed on the Rhode Island METRC API about lab test documents, dated, with the read-only probe that produced each number. It is not METRC documentation, it is not a statement about what any lab owes its customers, and it is not regulatory guidance: it describes how two endpoints behaved for licensed Rhode Island cultivators on the dates given.
The coverage counts belong to one day and to the packages three cultivators had tested by then; they move with every result a lab records and every document a lab attaches. What this page is about is the shape of the two calls, the field that links them, the content type of the response, and the rule that the document id is the lab’s to populate. Other states run their own METRC instances and can differ on every point below.
It is written for an operator who pays a lab portal for PDFs they already hold rights to through the state system, and for whoever builds the integration that reads them from METRC instead.
How every number on this site is observed, dated and bounded is on the methodology page, which also lists what SeedLink does not publish.
What the two calls return
Two calls answer the question. The first is one that an integration syncing potency already makes: GET /labtests/v2/results returns the lab results for a package, and every row carries a LabTestResultDocumentFileId. The second takes that id: GET /labtests/v2/labtestdocument/{id}?licenseNumber= returns the lab’s document itself. There is no lab portal in the path and no separate document catalogue to search; the id is on the results row, beside the potency numbers.
| Call | Parameters | Permissions | What comes back |
|---|---|---|---|
GET /labtests/v2/results | packageId (required), licenseNumber (required); paginated | View Packages, Manage Packages Inventory | One JSON row per result. Each row carries LabTestResultDocumentFileId and names the testing lab (LabFacilityName, LabFacilityLicenseNumber) |
GET /labtests/v2/labtestdocument/{id} | id in the path, licenseNumber (required) | View Packages, Manage Packages Inventory | The document as raw application/pdf bytes. One live document was 327,580 bytes and began with %PDF- |
PUT /labtests/v2/labtestdocument | licenseNumber (required); body rows of LabTestResultId, DocumentFileName, DocumentFileBase64 | View Packages, Manage Packages Inventory | The lab’s write: attaches a document to results it has already submitted. A cultivator integration never calls it |
The content type is the trap for anyone who has already integrated the manifest PDF. GET /transfers/v2/manifest/{id}/pdf returns JSON with the PDF inside it as base64; the lab document endpoint returns the bytes themselves. A client that parses every response as JSON cannot read it, and a client that base64-decodes the body is decoding a file that was never encoded. Read the body as bytes and check that it begins with %PDF-.
The permissions are the ones the results call already needs, View Packages and Manage Packages Inventory, per the METRC API reference. A credential that reads potency reads documents.
The document id is the lab’s, so coverage is a per-lab fact
LabTestResultDocumentFileId is populated by the lab that ran the test, not by METRC. METRC stores and serves whatever the lab attached, and when the lab attached nothing the field is empty and there is nothing to fetch. That makes document coverage a property of the lab, not of the cultivator, the package or the API.
On 2026-07-25 SeedLink read the results rows for every tested package across the three Rhode Island production cultivators it syncs, read-only, and grouped the rows by the lab named on them: 48 tested packages in all, tested by two labs. Packages tested by the first lab carried a document on 12 of 12. Packages tested by the second lab carried one on 0 of 36.
| Testing lab | Tested packages | Carrying a document | Share |
|---|---|---|---|
| First lab | 12 | 12 | 100% |
| Second lab | 36 | 0 | 0% |
| Cultivator | Tested packages | Carrying a document |
|---|---|---|
| First cultivator | 10 | 10 |
| Second cultivator | 16 | 0 |
| Third cultivator | 22 | 2 |
The per-cultivator split follows the per-lab split: which lab tested a package, not who owns it, decides whether a document exists. The lesson for anyone writing copy, a contract or a badge around this endpoint is that nothing may promise a COA on every tested package. On the day measured, that promise held for one cultivator and failed for two.
Whether the second lab attaches documents later, on request, or through a route other than the API was not observed. The field was empty on the day it was read, and that is all this page can say about it.
Siblings share a document: fetch once per item, store per package
Lab results in METRC propagate at the level of the item, not the package. When a lab records results against a package flagged IsTestingSample, METRC promotes every sibling package that shares the same ItemFromFacilityLicenseNumber and Item.Id to LabTestingState TestPassed, and it does so across facilities. In the Rhode Island sandbox, testing 24 lab-resident samples flipped all 160 cultivator-side packages in one pass. A cultivator has no receive step: METRC pushes the state server-side.
The document follows the same shape. On 2026-07-25 two packages of one item at one production facility both pointed at the same LabTestResultDocumentFileId. An integration that fetches per package downloads the same PDF once per sibling; an integration that fetches per item downloads it once and stores the reference on every package row.
Discovery costs nothing extra. The results call is one the sync already makes once per tested item, because on Rhode Island Item.UnitThcPercent is null and potency exists only in GET /labtests/v2/results; the document id rides on the rows that call already returns, and the results are fanned out to sibling packages the same way the potency is. The only added request is the document fetch, and it is made only when a package lacks a document.
| Step | Request | When it runs | Added cost |
|---|---|---|---|
| Read potency | GET /labtests/v2/results | Every sync, once per tested item; the results fan out to sibling packages | None beyond the sync itself |
| Learn the document id | None | It is LabTestResultDocumentFileId on the rows the potency read returned | None |
| Fetch the document | GET /labtests/v2/labtestdocument/{id} | Only when a package row lacks a document, once per item | One request per document |
| Store it | None | Once per item, referenced from every sibling package row | None |
How an integration should treat a document the cultivator may also have uploaded by hand
Cultivators upload their own COA PDFs too, and the two sources can collide on one package. SeedLink records two things per package: the storage path of whatever file is on offer, and the METRC document id that file came from, if any. Five combinations arise, and each has exactly one correct action.
| File on record | METRC document id on record | Action |
|---|---|---|
| None | None | Pull the document from METRC and record both |
| Present | None | The cultivator’s own upload. Never touch it |
| Present | Differs from the live LabTestResultDocumentFileId | Re-pull: the lab released a new document |
| Present | Same as the live id | Skip, with zero API calls |
| None | Present | The cultivator deleted the METRC-sourced document. Respect the deletion |
The last row is the one that gets missed. A delete button that clears the file and knows nothing about the METRC id leaves the package looking exactly like the first row, so the next hourly sync pulls the same document again and the delete appears not to work. Recording the id at pull time is what makes a deletion stick.
Two structural choices keep the rule honest. The document write lives outside the package mapper, the code that turns a METRC package into a catalog row on every sync, and that mapper never emits a storage path, a rule its tests assert; since the sync cannot write the file field, it cannot overwrite an upload. And the METRC-sourced file is stored at the same path shape as a manual upload, so the existing badge, download link and ownership check render it with no change.
What was not observed
A page that is cited has to be as clear about its edges as about its numbers. The following were not observed, and this page makes no claim about them.
- What
GET /labtests/v2/labtestdocument/{id}answers for an id whose document is missing, or for an id under a license that does not hold the package. No status code was recorded for either case. - Whether the second lab attaches documents later, on request, or through a route other than the API. The field was empty on 2026-07-25.
- Whether a lab can replace the file behind an existing id without the id changing. The ownership rule treats a changed id as a re-release and an unchanged id as unchanged content.
- Whether the Rhode Island sandbox lab facility attaches documents at all. The propagation counts above are sandbox; the coverage and byte counts are production.
- Whether
LabTestResultDocumentFileIdever differs between two siblings of one item. Two siblings sharing one id were observed; a counter-example was not looked for.
Check it yourself with two read-only GETs
The whole question is answerable with two GETs and no writes: one to learn whether your lab attached a document, one to fetch it.
GET https://api-ri.metrc.com/labtests/v2/results?licenseNumber=<your license>&packageId=<a tested package Id>
Authorization: Basic <base64 of VENDOR_KEY:USER_KEY>
for each row in the response:
note LabFacilityName and LabTestResultDocumentFileId
if the id is empty: this lab attached nothing to this result
GET https://api-ri.metrc.com/labtests/v2/labtestdocument/<that id>?licenseNumber=<your license>
Authorization: Basic <base64 of VENDOR_KEY:USER_KEY>
read the body as bytes, not as JSON
check that it begins with %PDF-Run the first call across every tested package you hold and group the rows by lab. If one lab’s rows carry an id and another’s do not, you have reproduced the finding on your own license. If every row carries an id, your lab attaches documents, and the portal fee is for a file the state system already serves you.
Questions
- Does METRC charge for the lab document?
- The document comes back through the same API and the same credential the cultivator already syncs with, and there is no lab portal in the path. On 2026-07-25 one GET returned 327,580 bytes of PDF at a Rhode Island production facility. This page says nothing about any lab’s own fees.
- Why do some tested packages have no document?
- Because
LabTestResultDocumentFileIdis populated by the lab, not by METRC. On 2026-07-25 one Rhode Island lab had attached a document to 12 of 12 tested packages and a second lab to 0 of 36. Whether the second lab attaches documents later, or by another route, was not observed. - Is the document response JSON?
- No.
GET /labtests/v2/labtestdocument/{id}returns rawapplication/pdfbytes, unlikeGET /transfers/v2/manifest/{id}/pdf, which wraps its PDF in JSON as base64. Read the body as bytes and check the leading%PDF-. - Do I need to fetch a document for every package?
- No. Lab results propagate at item level, and on 2026-07-25 two packages of one item pointed at the same document id. Fetch once per item and store the reference on every sibling package.
- Which permissions and parameters does the document call need?
- Per the METRC API reference,
licenseNumberis required and the permissions are View Packages and Manage Packages Inventory, the same two the results call needs. The id goes in the path. - What if the cultivator already uploaded their own COA for the package?
- Never touch it. A file on record with no METRC document id is the cultivator’s own upload, and the sync leaves it alone. Only a package with no file and no recorded id gets a pull.
- What if the cultivator deletes the COA that came from METRC?
- Respect the deletion. That is why the METRC document id is recorded at pull time: with it, a package with no file but a recorded id is left alone, and without it the next hourly sync pulls the same document again and the delete looks broken.
- Can the same call give me potency?
- Yes, and on Rhode Island it is the only place potency lives:
Item.UnitThcPercentis null, so the numbers exist only inGET /labtests/v2/results. The sync already makes that call once per tested item, which is why discovering a document id costs no extra request.
Sources and how to cite this page
- GET /labtests/v2/results and GET /labtests/v2/labtestdocument/{id} across the three Rhode Island production cultivators connected to SeedLink, read-only, 2026-07-25.
- One live lab document fetched at one Rhode Island production cultivator facility, 2026-07-25: byte count and leading bytes recorded.
- Rhode Island sandbox lab pipeline run: 24 lab-resident samples tested, 160 cultivator-side packages promoted; recorded without a date in docs/METRC.md § Lab Testing.
- SeedLink METRC field guide (docs/METRC.md): sections COAs come from METRC for free, Lab Testing (first bullet), and the Retry-After note on where potency lives.
- METRC API reference as recorded in the SeedLink repository (metrc-api-info/metrc-api-reference.md): GET /labtests/v2/results, GET /labtests/v2/labtestdocument/{id} and PUT /labtests/v2/labtestdocument.
SeedLink, "METRC serves the lab COA PDF for free in Rhode Island: one GET returns the raw PDF, and whether a document exists is a per-lab fact", observed 2026-07-25, revised 2026-09-20, https://www.seedlink.net/guides/metrc/free-coas
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/free-coas/data.json
Related
- METRC lab results in Rhode Island follow a package that was created before the results came back
- 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
- METRC’s StrainName is an ingredient, not the product name: Rhode Island requires a strain on infused items, so the two fields diverge by design
- All metrc field guides for rhode island
- Methodology: what SeedLink publishes and what it does not