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.
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.
Parameter
Type
Required
Notes
query
string
yes
Product wording, e.g. "pink kush 3.5g"
province
string
no
Scopes both the results and storeCount
limit
integer
no
Default 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.
Takes precedence over product; exact, drift-proof match
province
string
no
Omit 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.
Parameter
Type
Required
Notes
product
string
conditional
Required unless gtin given
gtin
string
conditional
Takes precedence over product
province
string
no
Applies to the average and the cheapest-store list
limit
integer
no
Cheapest 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.
Parameter
Type
Required
Notes
brand
string
yes
Brand name as catalogued
province
string
no
Omit 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.
Parameter
Type
Required
Notes
producerId
string
yes
Store identifier from find_stores_carrying_brand
province
string
no
—
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.
Parameter
Type
Required
Notes
brand
string
yes
Brand 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.
Parameter
Type
Required
Notes
metric
string
yes
One of: stores, products, producers, brands, categories
province
string
no
Omit for national
Use it to establish the denominator before quoting any penetration or coverage percentage.
Rank the biggest players in a market by how widely they are stocked.
Parameter
Type
Required
Notes
dimension
string
yes
One of: brand, product, category, subcategory
province
string
no
Omit for national
category
string
no
Scope the ranking to one product category
limit
integer
no
Default 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.
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.
Parameter
Type
Required
Notes
brand
string
yes
e.g. "Pure Sunfarms"
province
string
yes
Full name or two-letter code
activeSkuCount
integer
no
Override the observed SKU count
averageDistributionPct
number
no
Override the observed average distribution %
averageSkusPerRetailer
number
no
Override the observed average SKUs per retailer
salesMarketSharePct
number
no
Your 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.
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.
Tool
What it answers
get_my_portfolio
Your brands, their tracked SKUs, and which ones have no catalogue match. Call this first.
find_distribution_gaps
Stores in a province ranked by how much of your portfolio they do not carry, with the specific missing SKUs.
get_store_portfolio_detail
Your coverage at a single store, broken out by category and brand.
find_competitor_whitespace
Stores 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
Symptom
Cause
Fix
“No stores carry this” for a product you know exists
Loose wording passed to a name-matched tool
Call search_products first, pass the exact name or GTIN
Empty result for "AB"-style input on an older client
Province not normalized
Full names always work; codes are normalized server-side
isError: true with dimension_unsupported
Asked to rank by producer/LP
Rank by brand
HTTP 429
Rate limited (60/min)
Back off and retry
First call on a big province is slow
Cache miss on an expensive aggregate
Expected; 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.