Geo-search grocery stores by latitude/longitude radius, metro, state, or chain slug. Returns one row per matching store with full address, geocode, and chain metadata.
/v1/storesFree| Name | Type | Description |
|---|---|---|
lat | number | Latitude in decimal degrees, -90 to 90. Pair with lng + radius for a geo search. |
lng | number | Longitude in decimal degrees, -180 to 180. |
radius | numberdefault: 5 | Radius in miles. Only used when lat + lng are both supplied. Range 0.1–50. |
chain | string | Chain slug, e.g. heb, wegmans, lidl. Lowercase. |
state | string | Two-letter state code, e.g. NY, TX. |
metro | string | Metro key, e.g. nyc, sf, dallas. |
limit | integerdefault: 100 | Max rows returned. Range 1–500. |
offset | integerdefault: 0 | Cursor offset for pagination. |
include | string | Comma-separated opt-in fields: chain_online_config, hours, places_enrichment. Each adds a nested object on every row. |
include_count | booleandefault: false | When true, runs an extra COUNT(*) pass to populate the top-level count field. Off by default — the count query can't use the geo GIST index and adds ~50% latency. Pass true for paginated B2B feeds that need a total. |
lat+lng, chain, metro, or state) is recommended. An unfiltered call returns the first limit active stores ordered by id.curl 'https://api.mainmarket.com/v1/stores?lat=40.7128&lng=-74.0060&radius=5&include=hours'{
"count": 2,
"stores": [
{
"id": "8c1a4d1e-30a7-4d92-9e1c-1cb43c6f2e10",
"name": "Wegmans Brooklyn",
"display_name": "Wegmans Brooklyn",
"banner_name": "Wegmans",
"web_external_id": "59",
"chain_name": "Wegmans",
"chain_slug": "wegmans",
"chain_logo_url": "https://.../wegmans.png",
"address": "21 Flushing Ave",
"address_line_2": null,
"city": "Brooklyn",
"state": "NY",
"zip": "11205",
"country": "US",
"lat": 40.6982,
"lng": -73.9772,
"metro": "nyc",
"borough": "brooklyn",
"neighborhood": "Navy Yard",
"format": "supermarket",
"phone": "+17184181100",
"has_pickup": true,
"has_delivery": true,
"is_dark_store": false,
"is_active": true
}
]
}| Name | Type | Description |
|---|---|---|
id | uuid | Stable canonical store id. Use this everywhere downstream. |
name | string | Internal name, e.g. 'Wegmans Brooklyn'. |
display_name | string | null | Customer-facing label when distinct from name. |
banner_name | string | null | Sub-banner inside the chain (e.g. 'Mariano's' under Kroger). |
web_external_id | string | null | Chain's own store id (used in chain URLs and APIs). |
chain_name | string | Human chain name. |
chain_slug | string | URL-safe chain key. Use with /v1/chains/{slug}. |
chain_logo_url | string | null | Public CDN URL for the chain logo (PNG, transparent). |
address | string | Street address line 1. |
address_line_2 | string | null | Suite, unit, or floor when present. |
city, state, zip, country | string | Postal address. country is always 'US' today. |
lat, lng | number | WGS84 decimal degrees. |
metro | string | null | Metro region key, e.g. 'nyc'. |
borough, neighborhood | string | null | Sub-metro descriptors when geocoded. |
format | string | null | Store format (supermarket, express, dark_store, etc.). |
phone | string | null | E.164 phone number. |
has_pickup, has_delivery | boolean | Online fulfillment options. |
is_dark_store | boolean | True for fulfillment-only locations not open to walk-ins. |
is_active | boolean | False for stores we know are closed or being decommissioned. |
chain | object | null | Present when include=chain_online_config. Same shape as the ChainResponse documented under GET /v1/chains/{slug}. |
hours | array | null | Present when include=hours. Up to 7 rows of { day_of_week, open_time, close_time }. |
places | object | null | Present when include=places_enrichment. SerpAPI-sourced rating, review_count, popular_times, sentiment. |
Sourced from SerpAPI's Google Maps engine plus a one-time GPT sentiment pass. Seeded once per store — see places.enriched_at for freshness. Returns null entirely for stores that haven't been seeded yet, or whose SerpAPI match was low-confidence.
| Name | Type | Description |
|---|---|---|
rating | number | null | 0.0–5.0, one decimal place (Google rating). |
review_count | integer | null | Total Google reviews. |
typical_time_spent | string | null | Free-text, e.g. "People typically spend 15-45 min here". |
price_level | string | null | "$" / "$$" / "$$$". |
highlights | string[] | Flat de-duped tag list flattened from SerpAPI extensions (e.g. "Curbside pickup", "Wheelchair accessible"). Order preserved — the first 3-5 are typically the most distinctive. |
service_options | object | null | Map of { key: boolean }, e.g. { in_store_pickup: true, delivery: false }. Render only the truthy keys. |
popular_times | object | null | { current_day, graph }. graph has up to 7 keys (monday..sunday); each is a list of { time, busyness_score (0-100), label }. busyness_score = 0 = closed/no data. |
review_topics | object[] | Optional { keyword, mentions } list. Empty array when not returned by Google. |
sentiment | object | null | GPT structured-output sentiment: { score, label, summary, axes }. label ∈ Loved | Strong | Mixed | Frustrated. axes always has 5 keys (service / selection / value / cleanliness / operations), each { score, evidence } — score nullable when reviews don't mention that axis. |
enriched_at | ISO8601 string | null | When SerpAPI scraped this store. Use as a freshness indicator. |
places: null until the production seed (one-time, ~$150 for ~11.5k chain-active stores nationwide) ships. Design your UI to gracefully hide the section when places is null.ST_DWithin over a GIST index — sub-50ms for any radius up to 50 miles. Results are sorted by distance ascending when lat + lng are supplied.include_count=true opts into a second COUNT(*) pass. Off by default because the count can't share the geo GIST index used for the main query — leave off unless you actually paginate.GET /v1/stores/{id} if you need to resolve a stale id.| Name | Type | Description |
|---|---|---|
400 | Bad Request | lat or lng outside valid range, or radius outside 0.1–50. |
422 | Unprocessable Entity | Wrong type for limit/offset; state not 2 letters. |
500 | Internal Server Error | Unexpected server fault. Retry with backoff. |