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

Store aisles

Frequency-ranked aisles for one store. Cascades store-specific → chain-level → generic when fewer than 30% of products at the store carry an aisle tag.

GET/v1/stores/{store_id}/aisles$0.01

Path parameters

NameTypeDescription
store_idrequireduuidCanonical store id.

Query parameters

NameTypeDescription
limitintegerdefault: 20Max aisles to return. Range 1–100.

Request

Request
curl 'https://api.mainmarket.com/v1/stores/8c1a4d1e-30a7-4d92-9e1c-1cb43c6f2e10/aisles?limit=20'

Response

200 OKjson
{
  "store_id": "8c1a4d1e-30a7-4d92-9e1c-1cb43c6f2e10",
  "source": "store",
  "coverage": 0.78,
  "aisles": [
    "Aisle 1",
    "Aisle 2",
    "Aisle 3",
    "Produce",
    "Bakery",
    "Deli",
    "Meat",
    "Dairy",
    "Frozen"
  ],
  "aisle_counts": {
    "Aisle 1": 312,
    "Aisle 2": 287,
    "Produce": 185
  }
}

Response fields

NameTypeDescription
store_iduuidEcho of the requested store id.
sourcestringWhere the aisle list came from: store (this store has its own coverage), chain (aggregated from sibling stores in the same chain), or generic (PoS-noise-filtered fallback list).
coveragenumberFraction of store_product_prices rows at the source level that carry a non-null aisle (0.0–1.0).
aislesstring[]Normalized aisle names ordered by frequency descending.
aisle_countsobjectMap of aisle name → product count for diagnostics. Top 50 entries only.
ℹ
Aisle normalization
Names are normalized at query time: the "Aisle " prefix is stripped then re-prefixed when the remainder is purely numeric (so "Aisle 12", "12", and "AISLE 12, Shelf 3" all collapse to "Aisle 12"). PoS-noise rows (self-checkout, register candy, restrooms, etc.) are dropped.

How the cascade works

The endpoint runs three resolution attempts in order and returns the first one that produces a usable list:

1. Store-specific (source: store)
Pull every store_product_prices row at this store, run the normalization pipeline, count survivors. If the store has ≥30% coverage (i.e. at least 30% of its priced products carry a non-null aisle after normalization), return the per-store ranking. This is the gold standard — true to what's actually on this store's shelves.
2. Chain-aggregated (source: chain)
Triggered when store coverage is below 30%. Aggregates aisle frequencies across every store in the chain (sampled up to 50k rows) and returns the chain-level ranking. The order should still be a reasonable approximation for any store in the chain since chains generally lay out stores consistently. Identical output shape to Chain aisles.
3. Generic fallback (source: generic)
Last resort when even the chain has no aisle data. Returns an empty aisles array with coverage: 0.0. Means we don't have aisle metadata for this chain at all (typically because the chain only ships flyer prices without PDP-level aisle info). Show an "aisle data not available" affordance in your UI.
⚠
Inspect coverage before trusting the order
Coverage is your trust score. Above ~0.6 the order is reliable. Between 0.3 and 0.6 it's directional but the long tail is noisy — show top 5-10 aisles only. Below 0.3 the cascade fired and you're seeing chain-level data; trust it as a chain approximation, not a per-store map.

Workflow: drive an aisle picker

The standard shopping-app pattern. Pull aisles for the store, build a tab/picker UI, then load prices grouped by aisle on demand.

Request
import httpx

store_id = "8c1a4d1e-30a7-4d92-9e1c-1cb43c6f2e10"

# Step 1: get the aisle vocabulary
aisles_resp = httpx.get(
    f"https://api.mainmarket.com/v1/stores/{store_id}/aisles",
    params={"limit": 30},
    headers={"Authorization": "Bearer mm_live_..."},
).json()

# Step 2: gauge confidence and adapt UI
if aisles_resp["coverage"] < 0.2:
    print("Store doesn't have aisle data — hide the picker")
elif aisles_resp["source"] != "store":
    print(f"Using chain-level fallback (coverage {aisles_resp['coverage']:.0%})")

print("\nAisles:")
for name in aisles_resp["aisles"]:
    count = aisles_resp["aisle_counts"].get(name, "?")
    print(f"  {name}  ({count} products)")

Pair with /v1/prices for in-store routing

The aisles are most useful when joined to actual price rows. Pass ?normalize_aisles=true on /v1/prices and the aisle field comes back in the same vocabulary this endpoint returns — so you can sort a basket by aisle order without doing string mapping client-side.

In-store route plannerpython
import httpx

store_id = "8c1a4d1e-30a7-4d92-9e1c-1cb43c6f2e10"
basket = ["9d4e...", "a1b2...", "c3d4..."]

# Step 1: aisle order
aisles = httpx.get(
    f"https://api.mainmarket.com/v1/stores/{store_id}/aisles",
    params={"limit": 50},
    headers={"Authorization": "Bearer mm_live_..."},
).json()
order = {name: i for i, name in enumerate(aisles["aisles"])}

# Step 2: prices, normalized to the same vocabulary
prices = httpx.get(
    "https://api.mainmarket.com/v1/prices",
    params={"store_id": store_id, "product_ids": ",".join(basket), "normalize_aisles": True},
    headers={"Authorization": "Bearer mm_live_..."},
).json()

# Step 3: sort by walk order
prices["results"].sort(key=lambda r: order.get(r.get("aisle"), len(order)))
for row in prices["results"]:
    print(f"  {row.get('aisle') or '?':>10}  {row['name']}  ${row['price']:.2f}")

Notable behavior

  • One-shot cascade.If the store has <30% coverage, the response uses the chain aggregate only — it does not blend store and chain rows. Either it's a per-store list or a chain-level list, never both.
  • Chain aggregation samples up to 50,000 rows. For chains with richer-than-50k catalogs this is statistically equivalent (top aisles converge quickly) but the long tail can vary slightly between calls. Don't trust the tail beyond the top ~20.
  • Empty aisles array means generic. When source: "generic", the array is empty and coverage: 0.0. We don't ship a hardcoded "default grocery aisle list" because every chain's vocabulary is different — better to render "no aisle data" than to mislead the user.
  • Soft-deleted store ids resolve transparently. Same as Get a store: post-2026-04-25 dedup loser ids automatically resolve to their canonical survivor before the lookup runs.

Caching

Aisle distributions change very slowly. Responses send Cache-Control: public, max-age=600, stale-while-revalidate=3600 — a 10-minute cache with a 1-hour stale window. For mobile clients, persist by store_id + chain config_updated_at and refresh on chain config changes.

Related

  • Chain aisles — same shape but chain-wide. The cascade falls back to this when store coverage is low.
  • Search prices — pair with ?normalize_aisles=true for in-store route planning.
  • Get a store — store metadata including its chain slug, which feeds the chain-aisles fallback.

Errors

NameTypeDescription
404Not Foundstore_id does not exist.
422Unprocessable Entitylimit out of range.
402Payment RequiredPaid route — no payment proof.