Open MCP Preview · no key required

API Reference

Neobi's public API is MCP, in two tiers. The open tier below is nine read-only tools returning normalized Canadian cannabis retail market observations, each with lineage and freshness attached — no account, no API key. The customer tier adds portfolio intelligence for Neobi customers.

https://neobi-apim-cc.azure-api.net/open

Open tier last verified against production: September 2, 2026.

Access

Credentials
None — no account, no API key
Transport
Streamable HTTP MCP
Tools
9 read-only market tools
Rate limit
60 requests/min, 2,000/day per IP
Caching
~10 minutes
Status
Preview — no SLA; names and shapes may change

Conventions

These hold for every tool below.

Province input

Accepts a full name (Alberta) or two-letter code (AB). Omit it for a national view.

Errors

Validation failures return a tool result with isError: true and a structured body: {"error": "<code>", "message": "<human-readable>"}. The error codes below are stable. Calling an undefined tool fails at the protocol level with JSON-RPC error -32602.

Lineage

Every successful response carries a lineage block: source, filters (as actually applied), asOf (the timestamp of the underlying data — quote this, not “today”), sourceClass, and basis. Some responses add explicit caveats.

Store counts

Distinct retail locations — not banners, chains, or accounts.

Limits

60 requests/min and 2,000/day, both counted per caller IP — confirmed against the live APIM policy on 2026-09-02. Responses are cached ~10 minutes. A brand store list returns at most 500 stores; a ranking returns at most 100 rows.

Open tier tools

search_products

Resolve loose product wording to catalogued product names.

Call this first whenever a user names a product informally. The pricing and distribution tools match on the exact catalogued product name; querying them with a user's phrasing is the most common cause of a spurious “no stores carry this” answer. Resolve the wording here, then pass the returned productName or gtin onward.

ParameterTypeRequiredNotes
querystringyesProduct wording, e.g. "pink kush 3.5g"
provincestringnoScopes both the results and storeCount
limitintegernoDefault 10, clamped to 1–50

storeCount follows the province argument: with a province it counts stores in that province only (matching get_product_distribution for the same GTIN); without it the count is national. The applied scope is echoed in lineage.filters.storeCountScope.

Errorsquery_required
Request
{"name": "search_products", "arguments": {"query": "pink kush 3.5g", "province": "AB", "limit": 3}}
Response
{
  "items": [
    {"productName": "Pink Kush Dried Flower (7g)", "brand": "Pure Sunfarms", "gtin": "628188000133", "storeCount": 406},
    {"productName": "Pink Kush Dried Flower (28g)", "brand": "Pure Sunfarms", "gtin": "628188000935", "storeCount": 300}
  ],
  "lineage": {
    "source": "Neobi retail menu catalogue",
    "filters": {"province": "Alberta", "query": "pink kush 3.5g", "storeCountScope": "Alberta"},
    "asOf": "2026-08-23T13:22:25",
    "sourceClass": "observed_menu",
    "basis": "Directly observed retail menu listings — what stores publish, not what they sell."
  }
}

get_product_distribution

Count the retail stores carrying a product.

ParameterTypeRequiredNotes
productstringconditionalExact catalogued name; required unless gtin given
gtinstringconditionalTakes precedence over product; exact, drift-proof match
provincestringnoOmit for national

Supply gtin when known. A loosely-worded product name will under-count.

Errorsproduct_or_gtin_required

get_product_pricing

Get shelf-price position for a product: the average observed retail price plus the stores currently listing it cheapest.

ParameterTypeRequiredNotes
productstringconditionalRequired unless gtin given
gtinstringconditionalTakes precedence over product
provincestringnoApplies to the average and the cheapest-store list
limitintegernoCheapest stores to list; default 10, clamped 1–50

The cheapest-store list is only returned when a product name is given (it resolves stores by catalogued name). Prices are observed retail menu prices — not wholesale, landed, or promotional cost.

Known data artefact: a handful of listings carry placeholder prices a dollar or two above zero. They clear the sanity floor and can top the cheapest list; treat a shelf price far below the average as an artefact, not a deal.

Errorsproduct_or_gtin_required

find_stores_carrying_brand

List the retail stores carrying a brand, with store name, city, and province.

ParameterTypeRequiredNotes
brandstringyesBrand name as catalogued
provincestringnoOmit for national

Works for any brand in the catalogue, not only your own — this is public market observation, so it is the right tool for competitor footprint questions. Returns at most 500 stores. The store rows include a store identifier usable as producerId in get_store_assortment.

Errorsbrand_required

get_store_assortment

Describe what one retail store stocks: assortment by product category, and how its pricing sits against the provincial average for those categories.

ParameterTypeRequiredNotes
producerIdstringyesStore identifier from find_stores_carrying_brand
provincestringno

Note the naming: producerId is a store identifier, not a licensed-producer id.

ErrorsproducerId_required

get_brand_distribution_share

Get a brand's shelf presence, product by product.

ParameterTypeRequiredNotes
brandstringyesBrand name as catalogued

Returns the brand's products with their store counts and each product's share of observed shelf presence (sharePct).

This is share of shelf presence, not sales — it measures how many stores carry a listing and says nothing about units or revenue. Do not describe the result as market share.

Errorsbrand_required

get_market_size

Get a headline market count, served from a pre-computed daily market summary.

ParameterTypeRequiredNotes
metricstringyesOne of: stores, products, producers, brands, categories
provincestringnoOmit for national

Use it to establish the denominator before quoting any penetration or coverage percentage.

Errorsmetric_requiredmetric_unsupported
Request
{"name": "get_market_size", "arguments": {"metric": "stores", "province": "Alberta"}}
Response
{
  "metric": "stores",
  "count": 539,
  "lineage": {
    "source": "Neobi market summary",
    "filters": {"province": "Alberta"},
    "asOf": "2026-08-23T13:26:57",
    "sourceClass": "derived_rollup",
    "basis": "Counts rolled up from observed retail menus."
  }
}

rank_by_distribution

Rank the biggest players in a market by how widely they are stocked.

ParameterTypeRequiredNotes
dimensionstringyesOne of: brand, product, category, subcategory
provincestringnoOmit for national
categorystringnoScope the ranking to one product category
limitintegernoDefault 10, maximum 100

Ranking by licensed producer is deliberately unavailable — the underlying data records the retailer operating the store, not the cannabis producer, so a “producer” ranking would return retail banners. Rank by brand instead. Asking for an unsupported dimension returns dimension_unsupported with that explanation. A cache-miss on a large province can take ~20 seconds; repeat calls are instant.

This is a shelf-presence ranking, not a sales ranking; the response is labelled rankedBy: "distribution" for exactly that reason.

Errorsdimension_requireddimension_unsupported
Request
{"name": "rank_by_distribution", "arguments": {"dimension": "brand", "province": "Alberta", "category": "pre-roll", "limit": 5}}
Response
{
  "items": [
    {"label": "Back Forty", "value": 519},
    {"label": "General Admission", "value": 512},
    {"label": "SHRED", "value": 509}
  ],
  "lineage": {"filters": {"province": "Alberta", "category": "pre-roll", "dimension": "brand", "rankedBy": "distribution"}, "...": "..."}
}

analyze_brand_pod_share

Analyze a brand's share of available points of distribution (PODs) in a province.

A POD is one SKU listed at one retail store: a brand with 100 SKUs in 300 stores occupies far more PODs than one with 5 SKUs everywhere.

ParameterTypeRequiredNotes
brandstringyese.g. "Pure Sunfarms"
provincestringyesFull name or two-letter code
activeSkuCountintegernoOverride the observed SKU count
averageDistributionPctnumbernoOverride the observed average distribution %
averageSkusPerRetailernumbernoOverride the observed average SKUs per retailer
salesMarketSharePctnumbernoYour sales share, for the efficiency index — never sourced from Neobi

Two frameworks are returned, and they are not the same thing: the observed block counts real distinct store–SKU pairs in Neobi's data; the brandMetrics block runs the classic napkin calculation using Neobi-derived inputs unless you override them. A large divergence between the two usually means the assumed inputs don't match the market. Counting rules: a SKU counts once per store however often it is listed; delisted history is excluded; a listed product momentarily out of stock still occupies its POD; accessories and non-cannabis items are excluded. A cache-miss can take ~30 seconds; repeat calls are instant.

Supply salesMarketSharePct to get a distribution efficiency index (sales share ÷ POD share; above 1 means sales share exceeds shelf share). Neobi holds no point-of-sale data, so that figure can only come from you and is always labelled caller_supplied. Without it, the comparison block is omitted entirely.

Errorsbrand_requiredprovince_requiredprovince_unrecognized

Customer tier

Portfolio intelligence on the same market data. Every tool here needs to know which brands are yours, which is why none of them are in the open tier.

https://neobi-apim-cc.azure-api.net/customer

Two credentials doing two different jobs. The Ocp-Apim-Subscription-Key header identifies the calling application and carries the rate limit — it is not identity. The Authorization: Bearer token identifies you, and is what resolves organization, entitlement and portfolio. A subscription key on its own gets you nothing on this tier. Limits are 300 requests/min and 20,000/day per subscription.

ToolWhat it answers
get_my_portfolioYour brands, their tracked SKUs, and which ones have no catalogue match. Call this first.
find_distribution_gapsStores in a province ranked by how much of your portfolio they do not carry, with the specific missing SKUs.
get_store_portfolio_detailYour coverage at a single store, broken out by category and brand.
find_competitor_whitespaceStores carrying a competitor but not your brand. For the purely market question of who stocks a brand, the open tier answers it without an account.

Four tools, documented from the deployed contract but not yet wire-verified from this site — that needs a real subscription key and JWT. Parameter-level reference for this tier is still to come; the open tier above is verified against production. Talk to the Neobi team to get access.

Failure modes worth knowing

SymptomCauseFix
“No stores carry this” for a product you know existsLoose wording passed to a name-matched toolCall search_products first, pass the exact name or GTIN
Empty result for "AB"-style input on an older clientProvince not normalizedFull names always work; codes are normalized server-side
isError: true with dimension_unsupportedAsked to rank by producer/LPRank by brand
HTTP 429Rate limited (60/min)Back off and retry
First call on a big province is slowCache miss on an expensive aggregateExpected; the retry is instant

Data methodology

Every number here is observed retail menu presence — what stores publish, not what they sell. Neobi holds no point-of-sale data, so shelf presence must never be described as sales market share. Full methodology, freshness behaviour, and the access model live in the neobi-mcp repository. Enterprise and customer-scoped access is available through the Neobi team.