Search
- Search
- What
/searchDoes - Query Formats
- Endpoint Catalog
- Validation Rules
- Response Shape
- Tuning Result Volume with
pageSize - Filtering Noise with
minScore - Streaming Responses (SSE)
- Limits and Behavior
- Examples
- Next Steps
Search¶
The /v2/search endpoint provides a single text-search entry point across the most commonly searched Metrc collections. Hand it a tag, a manifest number, an item name, or a partial label, and it returns a ranked list of every matching object across the relevant Metrc collections — without you having to know which collection to look in.
A T3+ subscription is not required to use this endpoint.
What /search Does¶
Each request goes through five steps:
- Format detection. The query string is classified into one or more formats:
DATETIME,HEX_STRING,INTEGER, orBASIC_STRING. A query can match multiple formats — for example,"12345"is bothINTEGERandHEX_STRING. - Endpoint selection. If
endpointIdsis omitted, the 17 default-enabled endpoints are used. IfendpointIdsis provided, exactly that set is used (including any endpoints that are off by default). - Format gating. Endpoints whose searchable fields don't accept any of the detected query formats are removed from the request and reported in
skippedMetrcEndpointIds. For example,"Blue Dream"(BASIC_STRING) skips transfer endpoints, which only searchmanifestNumber. - Parallel fan-out. The remaining endpoints are queried in parallel against Metrc. Each endpoint is asked for at most
pageSizematches (default 10, max 100). - Score and sort. Each matched object is scored using the query length, the matched field's length, and the position of the match within that field. Results from all endpoints are merged into a single list and sorted by
scoredescending.
Query Formats¶
Format detection determines which endpoints are eligible. The detection is greedy — a single query can satisfy multiple formats simultaneously, in which case it unlocks the union of their endpoints.
| Format | Detection rule | Example |
|---|---|---|
DATETIME | ISO 8601 date or datetime: ^\d{4}-\d{2}-\d{2}(T...)?$ | 2024-07-17 |
HEX_STRING | Starts with 1A (Metrc tag prefix) or is all hex characters (0-9a-fA-F) | 1A4FF0100000022000000123 |
INTEGER | All digits | 12345 |
BASIC_STRING | Anything that doesn't match a more specific format above | Blue Dream |
Note
No endpoint currently searches date fields. A pure DATETIME query (e.g. 2024-07-17) returns an empty data array with every targeted endpoint reported in skippedMetrcEndpointIds. Use a tag, label, manifest number, or name instead.
Endpoint Catalog¶
The 26 endpoints /search can query, what each one searches, the formats it accepts, and whether it's queried by default.
| Endpoint ID | Searches | Accepted formats | Default |
|---|---|---|---|
AVAILABLE_TAGS | label | HEX_STRING, INTEGER, BASIC_STRING | ✅ |
USED_TAGS | label | HEX_STRING, INTEGER, BASIC_STRING | ✅ |
VOIDED_TAGS | label | HEX_STRING, INTEGER, BASIC_STRING | ❌ |
ACTIVE_ITEMS | name (also scores strainName) | BASIC_STRING | ✅ |
ACTIVE_LOCATIONS | name | BASIC_STRING | ✅ |
ACTIVE_STRAINS | name | BASIC_STRING | ✅ |
ACTIVE_HARVESTS | name | BASIC_STRING | ✅ |
INACTIVE_HARVESTS | name | BASIC_STRING | ❌ |
ACTIVE_PLANTBATCHES | name | BASIC_STRING | ✅ |
INACTIVE_PLANTBATCHES | name | BASIC_STRING | ❌ |
ACTIVE_PACKAGES | label | HEX_STRING, INTEGER, BASIC_STRING | ✅ |
INTRANSIT_PACKAGES | label | HEX_STRING, INTEGER, BASIC_STRING | ✅ |
INACTIVE_PACKAGES | label | HEX_STRING, INTEGER, BASIC_STRING | ❌ |
TRANSFERRED_PACKAGES | packageLabel | HEX_STRING, INTEGER, BASIC_STRING | ✅ |
VEGETATIVE_PLANTS | label | HEX_STRING, INTEGER, BASIC_STRING | ✅ |
FLOWERING_PLANTS | label | HEX_STRING, INTEGER, BASIC_STRING | ✅ |
INACTIVE_PLANTS | label | HEX_STRING, INTEGER, BASIC_STRING | ❌ |
ACTIVE_MOTHER_PLANTS | label | HEX_STRING, INTEGER, BASIC_STRING | ✅ |
INACTIVE_MOTHER_PLANTS | label | HEX_STRING, INTEGER, BASIC_STRING | ❌ |
INCOMING_ACTIVE_TRANSFERS | manifestNumber | INTEGER, BASIC_STRING | ✅ |
INCOMING_INACTIVE_TRANSFERS | manifestNumber | INTEGER, BASIC_STRING | ❌ |
OUTGOING_ACTIVE_TRANSFERS | manifestNumber | INTEGER, BASIC_STRING | ✅ |
OUTGOING_INACTIVE_TRANSFERS | manifestNumber | INTEGER, BASIC_STRING | ❌ |
OUTGOING_REJECTED_TRANSFERS | manifestNumber | INTEGER, BASIC_STRING | ✅ |
ACTIVE_SALES | receiptNumber | INTEGER, BASIC_STRING | ✅ |
INACTIVE_SALES | receiptNumber | INTEGER, BASIC_STRING | ❌ |
To opt into off-by-default endpoints (INACTIVE_*, VOIDED_TAGS), pass them explicitly via endpointIds. Providing endpointIds overrides the default set entirely — if you want to add off-by-default endpoints to the defaults, you have to list every endpoint you want.
Validation Rules¶
/search rejects requests that can't produce useful results, with a 400 Bad Request. The most common rejections:
- Empty
query(or whitespace-only) — there's no fan-out target. Provide a real query string. - Pure ISO 8601 date/datetime
query(e.g.2024-07-17,2024-07-17T20:26:07) — no endpoint searches date fields, so the request would silently return empty. Use a tag, label, manifest number, or name instead. - Unknown
endpointIds— the error message names the specific unrecognized IDs (not the whole list you sent). - Out-of-range
pageSize— must be an integer in[1, 100]. - Out-of-range
minScore— must be a number in[0.0, 1.0].
Response Shape¶
Every successful response has the same seven top-level keys.
{
"data": [
{
"endpointId": "ACTIVE_PACKAGES",
"score": 0.7142857142857143,
"matchedEntry": { "...": "..." }
},
{
"endpointId": "TRANSFERRED_PACKAGES",
"score": 0.0833333333333333,
"matchedEntry": { "...": "..." }
}
],
"queriedMetrcEndpointIds": [
"ACTIVE_PACKAGES",
"INTRANSIT_PACKAGES",
"TRANSFERRED_PACKAGES",
"AVAILABLE_TAGS"
],
"failedMetrcEndpointIds": [],
"skippedMetrcEndpointIds": [
"ACTIVE_ITEMS",
"ACTIVE_LOCATIONS"
],
"detectedQueryFormats": ["HEX_STRING", "INTEGER"],
"endpointMetadata": [
{ "endpointId": "ACTIVE_PACKAGES", "truncated": true, "totalAvailable": 47 },
{ "endpointId": "INTRANSIT_PACKAGES", "truncated": false, "totalAvailable": 2 },
{ "endpointId": "TRANSFERRED_PACKAGES", "truncated": false, "totalAvailable": 1 },
{ "endpointId": "AVAILABLE_TAGS", "truncated": false, "totalAvailable": 0 }
],
"request": {
"query": "1A4FF...",
"licenseNumber": "LIC-00001",
"pageSize": 10,
"minScore": 0.0
}
}
data is sorted by score descending. The shape of each matchedEntry depends on its endpointId — a match from ACTIVE_PACKAGES looks like a Metrc package, a match from INCOMING_ACTIVE_TRANSFERS looks like a Metrc transfer. Always check endpointId before using a result.
queriedMetrcEndpointIds vs failedMetrcEndpointIds vs skippedMetrcEndpointIds¶
| Field | Meaning |
|---|---|
queriedMetrcEndpointIds | Endpoints that were called against Metrc and returned successfully. Their results are in data. |
failedMetrcEndpointIds | Endpoints that were called but errored — Metrc API error, timeout, or insufficient license permissions. Their results are missing. |
skippedMetrcEndpointIds | Endpoints that were not called because the detected query format isn't compatible with any of their fields. No Metrc traffic was generated. |
Together, the three arrays account for every endpoint the request would have touched. Use detectedQueryFormats if you need to understand why an endpoint was skipped.
endpointMetadata and the truncated Flag¶
Each successful endpoint contributes one entry to endpointMetadata, naming itself, whether its response was truncated, and Metrc's reported total count.
| Field | Meaning |
|---|---|
endpointId | The Metrc endpoint this entry describes — one of the IDs in queriedMetrcEndpointIds. |
truncated | true when Metrc had more matches than pageSize returned. Raise pageSize to retrieve them. |
totalAvailable | Metrc's reported total count for the underlying query — the size of the unbounded result set. |
Failed and skipped endpoints are omitted (no Metrc response to derive from).
A typical flow: receive the response, find any endpointMetadata entry with truncated: true, and decide whether to re-issue the search with a larger pageSize or scope it down with endpointIds.
request Echo¶
The request object echoes the resolved request parameters — values after server-side defaults and trimming. Useful for:
- Debugging proxied or cached responses where the original URL is no longer obvious.
- Clients that fan out multiple concurrent searches and need to match responses to requests.
- Confirming that a default value (e.g.
pageSize: 10,minScore: 0.0) was actually applied as expected.
endpointIds isn't echoed — the resolved set is queriedMetrcEndpointIds plus skippedMetrcEndpointIds.
Tuning Result Volume with pageSize¶
The pageSize parameter controls how many matches each downstream Metrc endpoint may return. It defaults to 10 and is capped at 100. The total number of items in data is at most len(queriedMetrcEndpointIds) * pageSize.
Use a larger pageSize when the default isn't enough — for example, when a license has many similarly-named items and the top 10 from each endpoint isn't covering the matches you're looking for. Use a smaller pageSize when the request is purely a "did anything match" check.
curl 'https://api.trackandtrace.tools/v2/search?licenseNumber=LIC-00001&query=Blue%20Dream&pageSize=50' \
-H 'Authorization: Bearer <TOKEN>'
pageSize applies uniformly to every queried endpoint. Out-of-range values (< 1 or > 100) return 400 Bad Request.
If you're not sure whether a higher pageSize would yield more results, look at endpointMetadata in the response: any entry with truncated: true had more matches available than your pageSize returned, and totalAvailable reports Metrc's full count.
Filtering Noise with minScore¶
The minScore query parameter drops results whose relevance score is below the threshold, server-side. Scores are in [0, 1], where 1.0 is an exact full-string match. Defaults to 0.0 (no filtering).
curl 'https://api.trackandtrace.tools/v2/search?licenseNumber=LIC-00001&query=Blue%20Dream&minScore=0.1' \
-H 'Authorization: Bearer <TOKEN>'
Useful when:
- A short or common query string produces a long tail of low-score matches that the UI just discards anyway.
- You're looking for "is there a strong match?" and want the server to filter out the dross before serializing it.
The filter is per-result and applied before sorting. In SSE mode, result events still fire even when every match was filtered out — the event's results array will simply be empty. That tells the client the endpoint completed without truncation lying about availability.
Streaming Responses (SSE)¶
For interactive UIs that want to render results as soon as each Metrc endpoint replies — instead of waiting for the slowest one — /search can stream Server-Sent Events. There are two equivalent ways to opt in:
- Add
?stream=trueto the URL (easy to test fromcurl, easy to document). - Send
Accept: text/event-stream(whatEventSourcedoes natively).
The response is a text/event-stream body containing two kinds of events:
event: result— emitted once per completed downstream endpoint, in completion order (fastest first). Payload containsendpointId,status(successorfailed), anerrorstring (failures only), and aresultsarray sorted byscoredescending.event: done— emitted once after every downstream endpoint has finished. Payload mirrors the JSON-mode summary:queriedMetrcEndpointIds,failedMetrcEndpointIds,skippedMetrcEndpointIds, anddetectedQueryFormats.
Sample stream:
event: result
data: {"endpointId":"AVAILABLE_TAGS","status":"success","results":[{"score":1.0,"matchedEntry":{"...":"..."}}],"truncated":false,"totalAvailable":1}
event: result
data: {"endpointId":"ACTIVE_PACKAGES","status":"success","results":[{"score":0.71,"matchedEntry":{"...":"..."}}],"truncated":true,"totalAvailable":47}
event: result
data: {"endpointId":"INTRANSIT_PACKAGES","status":"failed","error":"Metrc Connection Failed","results":[]}
event: done
data: {"queriedMetrcEndpointIds":["AVAILABLE_TAGS","ACTIVE_PACKAGES"],"failedMetrcEndpointIds":["INTRANSIT_PACKAGES"],"skippedMetrcEndpointIds":["ACTIVE_ITEMS"],"detectedQueryFormats":["HEX_STRING","INTEGER"],"request":{"query":"1A4FF...","licenseNumber":"LIC-00001","pageSize":10,"minScore":0.0}}
Browser usage with EventSource:
const url = `/v2/search?licenseNumber=LIC-00001&query=${encodeURIComponent('Blue Dream')}`;
const source = new EventSource(url, { withCredentials: true });
source.addEventListener('result', (e) => {
const { endpointId, status, results } = JSON.parse(e.data);
// Render incrementally
});
source.addEventListener('done', (e) => {
source.close();
});
curl usage:
curl -N 'https://api.trackandtrace.tools/v2/search?licenseNumber=LIC-00001&query=Blue%20Dream&stream=true' \
-H 'Authorization: Bearer <TOKEN>'
(-N disables curl's output buffering so events appear as they arrive.)
Note
The SSE stream emits results in completion order, not score order. Each event's own results array is locally sorted by score, but the global ranking that JSON mode produces requires the client to merge events as they arrive.
Limits and Behavior¶
- Per-endpoint cap of
pageSizematches. Each downstream endpoint returns at mostpageSizeresults (default 10, max 100). The total number of items indatais at mostlen(queriedMetrcEndpointIds) * pageSize. WatchendpointMetadata[].truncatedto know when raisingpageSizewould yield more. - Per-endpoint timeout (~10s). Each downstream Metrc call is bounded so a single slow endpoint can't dominate total response time. Endpoints that exceed the timeout land in
failedMetrcEndpointIdswith anerrorof"Metrc Request Timeout". In SSE mode the correspondingresultevent still fires — failed endpoints don't block the rest of the stream. - Latency varies by endpoint. Tag, item, location, and strain searches typically return in well under a second. Package, plant, and especially transfer searches can take several seconds. JSON-mode total request time is bounded by the per-endpoint timeout; SSE-mode delivers fast endpoints' results first.
- Narrow scope for speed. If you know the kind of object you're after, pass
endpointIdsto drop everything else. A query againstendpointIds=AVAILABLE_TAGS,USED_TAGSwill be much faster than the default fan-out. - Per-license. Like every collection endpoint,
/searchis scoped to thelicenseNumberquery parameter. Searching across multiple licenses requires multiple requests. - Free. No T3+ subscription required.
Examples¶
Substitute your API token (Authorization: Bearer ...) and license number into each example.
Tag scan — find every collection a tag appears in:
curl 'https://api.trackandtrace.tools/v2/search?licenseNumber=LIC-00001&query=1A4FF0100000022000000123' \
-H 'Authorization: Bearer <TOKEN>'
Manifest number lookup — find a transfer by its manifest number:
curl 'https://api.trackandtrace.tools/v2/search?licenseNumber=LIC-00001&query=1234566' \
-H 'Authorization: Bearer <TOKEN>'
This query is detected as HEX_STRING and INTEGER, so it searches every endpoint that accepts either — packages, plants, tags, transfers, and so on.
Item or strain name search — narrowed to items and strains for speed:
curl 'https://api.trackandtrace.tools/v2/search?licenseNumber=LIC-00001&query=Blue%20Dream&endpointIds=ACTIVE_ITEMS,ACTIVE_STRAINS' \
-H 'Authorization: Bearer <TOKEN>'
Include inactive packages in a label search:
curl 'https://api.trackandtrace.tools/v2/search?licenseNumber=LIC-00001&query=0000123&endpointIds=ACTIVE_PACKAGES,INTRANSIT_PACKAGES,INACTIVE_PACKAGES' \
-H 'Authorization: Bearer <TOKEN>'
Next Steps¶
- Browse the full OpenAPI Spec for the formal request/response schema.
- See API Scripts for examples of building higher-level workflows on top of
/search.