MainMarketAPI Reference
Get Access
Overview
  • Introduction
  • Authentication
  • Errors
Stores
  • List stores
  • Get a store
  • Store sentiment
  • Store coupons
  • Store aisles
Chains
  • List chains
  • Get a chain
  • Chain aisles
  • Resolve a list
Products
  • Catalog search
  • Get a product
  • Coupons for product
Prices
  • Search prices
  • Prices by UPC
  • Prices at a store
  • Cheapest nearby
Coupons
  • List coupons
  • Get a coupon
  • Coupon savings
Indices
  • Published indices
Discovery & meta
  • Discovery routes
  • OpenAPI spec
  • Agent skill spec
Overview
  • Introduction
  • Authentication
  • Errors
Stores
  • List stores
  • Get a store
  • Store sentiment
  • Store coupons
  • Store aisles
Chains
  • List chains
  • Get a chain
  • Chain aisles
  • Resolve a list
Products
  • Catalog search
  • Get a product
  • Coupons for product
Prices
  • Search prices
  • Prices by UPC
  • Prices at a store
  • Cheapest nearby
Coupons
  • List coupons
  • Get a coupon
  • Coupon savings
Indices
  • Published indices
Discovery & meta
  • Discovery routes
  • OpenAPI spec
  • Agent skill spec

Catalog search

Search the 528k-row canonical product catalog without store context. Trigram text search ranked by KNN similarity, plus exact filters (UPC, product_id, brand, category). Returns one row per canonical product with full enrichment fields. The discovery layer above pricing — resolve a fuzzy query to a canonical product_id once, then drive everything else from that id.

GET/v1/products$0.01
ℹ
Catalog vs prices vs coupons
This endpoint returns products (canonical SKUs). It doesn't know what anything costs or where it's sold — that's Search prices. It doesn't know what coupons attach to a product — that's Coupons for product. Use this endpoint first to resolve a fuzzy query into one or more product_id UUIDs, then pass those into the price/coupon endpoints.

Five query modes

The route accepts five filter modes, each tuned for a different workflow. Filter precedence when multiple are sent:

?ids=<uuid>,<uuid>,...
Batch hydrate. Highest precedence — overrides every other filter. Cap 50 ids per call. Used by Cart's Pantry to hydrate metadata for many basket rows in one call. Direct PK lookup, fastest path.
?q=<text>
Trigram text search using pg_trgm GIN index. KNN-ranked: pulls a 100-row name pool + 100-row brand pool, dedupes, returns top matches by similarity. Best for "find me oat milk" — handles typos, partial matches, and brand-vs-name ambiguity. Combine with brand or category to narrow oversampled candidates.
?barcode=<upc>
Lookup by GS1 UPC or EAN. 6-32 digits. Matches both raw upc and zero-padded upc_normalized — pass whatever format you have. The path of choice when scanning a barcode.
?product_id=<uuid>
Single canonical product by MainMarket UUID. Cheapest path. See Get a product for the focused workflow page.
?brand=<name> + ?category=<name>
Direct filter path (no text-search rank). Returns products matching the exact filter combo. Use to enumerate "every Cheerios variant" or "every cereal we have." ILIKE-based; case-insensitive but exact substring.

Query parameters

NameTypeDescription
qstringTrigram text search on name (and brand fallback). 1–100 characters.
barcodestringUPC or EAN. 6–32 digits. Matches both raw upc and zero-padded upc_normalized.
product_iduuidSingle product UUID lookup.
idsstringComma-separated product UUIDs (max 50). When set, overrides every other filter and returns the requested rows in input order.
brandstringExact brand match. Case-insensitive.
categorystringExact category match. Case-insensitive.
limitintegerdefault: 501–500.
offsetintegerdefault: 0Cursor offset.
⚠
At least one filter required
Unfiltered calls return 422 Unprocessable Entity. The catalog has 528k+ rows; a bare SELECT * would be unfair to other tenants.

Request

Request
curl 'https://api.mainmarket.com/v1/products?q=cheerios&limit=10'

Response

200 OKjson
{
  "count": 1,
  "results": [
    {
      "product_id": "9d4e1c80-78a3-4a6d-9b1f-2cdb3d0c7e90",
      "name": "Cheerios Cereal, 18 oz",
      "brand": "Cheerios",
      "size_display": "18 oz",
      "image_url": "https://.../cheerios.jpg",
      "upc": "00016000275287",
      "category": "Cereal",
      "description": "Whole grain oats cereal.",
      "ingredients": "Whole grain oats, corn starch, sugar, salt, ...",
      "allergens": ["wheat"],
      "dietary_labels": ["heart_healthy"],
      "snap_eligible": true,
      "serving_size": "39",
      "serving_size_unit": "g"
    }
  ]
}

Product fields

NameTypeDescription
product_iduuidStable canonical product id.
namestringCanonical product name.
brandstring | nullManufacturer brand. Sparse for private label.
size_displaystring | nullCustomer-facing size, e.g. '12 oz', '4 ct'.
image_urlstring | nullPublic CDN image URL.
upcstring | nullGS1 UPC, raw chain format.
categorystring | nullInternal category label.
descriptionstring | nullLong-form product description, when scraped.

Enrichment fields (sparse, scraped from PDPs)

NameTypeDescription
ingredientsstring | nullFree-text ingredient list as printed on the package. Sourced from chains that expose ingredients on PDPs (e.g. Wegmans, H-E-B, Whole Foods); null elsewhere.
allergensstring[]Allergen tags. Empty array (not null) when none are listed. Includes both clean tags ('wheat', 'milk') and raw label-disclosure phrases — clients should treat the array as an unordered set, not a precise contract.
dietary_labelsstring[]Tags such as gluten_free, organic, vegan, kosher, heart_healthy. Empty array when none are listed.
snap_eligibleboolean | nullTrue/false when known. Null when not yet classified — the column is sparsely populated today.
serving_size, serving_size_unitstring | nullServing size as printed (e.g. "39" + "g"). Combine client-side for display.
ℹ
What we don't return
Per-nutrient values (calories, fats, sodium, carbs, sugars, protein) are not currently returned from this endpoint — the production catalog only carries free-text ingredients, allergens, dietary labels, SNAP eligibility, and serving-size strings. Per-nutrient extraction is on the roadmap; ask if you need it.

Workflow: typeahead + ranked search

Request
import httpx

# Fast, ranked text search — "what does the user mean by 'cherrios'?"
r = httpx.get(
    "https://api.mainmarket.com/v1/products",
    params={"q": "cherrios", "limit": 5},  # typo intentional
    headers={"Authorization": "Bearer mm_live_..."},
).json()

for p in r["results"]:
    print(f"  {p['name']}  [{p.get('brand', '?')}]  {p.get('size_display', '')}")

# Use the top result's UUID for follow-up queries (prices, coupons, etc.)
top = r["results"][0]
print(f"\nResolved → {top['product_id']}")

Workflow: hydrate a Cart pantry

The mass-lookup pattern. A Cart user's pantry might hold 30+ products by id; this hydrates them in one call instead of 30 round-trips.

Batch hydratepython
import httpx

pantry_ids = ["9d4e1c80-...", "a1b2c3d4-...", "5e6f7g8h-..."]  # up to 50

r = httpx.get(
    "https://api.mainmarket.com/v1/products",
    params={"ids": ",".join(pantry_ids)},
    headers={"Authorization": "Bearer mm_live_..."},
).json()

# Note: order is NOT preserved; map by id if order matters.
by_id = {p["product_id"]: p for p in r["results"]}
for pid in pantry_ids:
    p = by_id.get(pid)
    if p is None:
        print(f"  {pid[:8]}...  (not found)")
    else:
        print(f"  {p['name']}  {p.get('size_display', '')}")

Workflow: enumerate a brand

Pull every variant of a brand (every Cheerios SKU, every Heinz SKU). Useful for comparison shopping screens or building a brand catalog.

curlbash
# Every Cheerios product
curl 'https://api.mainmarket.com/v1/products?brand=Cheerios&limit=50' \
  -H "Authorization: Bearer mm_live_..."

# Narrow with a category
curl 'https://api.mainmarket.com/v1/products?brand=Cheerios&category=Cereal&limit=50' \
  -H "Authorization: Bearer mm_live_..."

Notable behavior

  • Trigram KNN ranking. q uses pg_trgm GIN-indexed similarity. Top matches are pulled from a 100-row name pool plus 100-row brand pool, then deduped — fast for short queries, deterministic for long ones. Sub-50ms p50 even on the full catalog.
  • ids= overrides every other filter. If you pass ids=...&q=cheerios, the q is ignored. This is a deliberate guardrail so the Pantry hydrate path can't accidentally fall into a slow text scan.
  • Order is not preserved. For ids= calls, results come back in DB-insert order, not your input order. Map by product_id if you need to preserve sequence.
  • Unknown ids are silently dropped. Pass a list of 30 ids and get 28 results back — the missing ones are gone (deleted, never existed, malformed UUID). No 404. By design — a stale pantry row shouldn't break the page load.
  • Enrichment fields are sparse. ingredients, allergens, dietary_labels, snap_eligible, and serving size are all populated by chain-specific PDP scrapes. Wegmans / H-E-B / Whole Foods tend to populate them; Kroger / Lidl typically don't. Treat as opportunistic enrichment, not a guarantee.
  • Cache aggressively. Catalog metadata is slow-moving. Responses send Cache-Control: public, max-age=60, stale-while-revalidate=300. Honor that to cut cost.

Combining filters

?q + ?brand
Trigram search narrowed to one brand. Useful when a brand has many variants and you want to find "the right Cheerios" without matching cross-brand similar products.
?q + ?category
Trigram search within a category. The brand+category combo is the standard 'show me cereals from Brand X' pattern.
?brand + ?category (no q)
Direct filter path — no text rank, just exact matches sorted by name ascending. Returns the canonical "every Brand X cereal" list deterministically.

Related

  • Get a product — focused single- product workflow page with all four lookup modes documented.
  • Coupons for product — every active coupon attached to a product.
  • Search prices — pricing for resolved products across stores and chains.

Errors

NameTypeDescription
422Unprocessable EntityNo filter supplied, or barcode outside 6–32 chars, or ids list >50 items.
402Payment RequiredPaid route — no payment proof.
500Internal Server ErrorUnexpected server fault.