{"openapi":"3.1.0","info":{"title":"GroceryPulse shopper comparison API","version":"1.0.0","description":"Read-only, keyless comparisons for personal shopping agents. Explicit supported city and canonical list required. Current chain-level public shopper projection only; no raw observations, history, CGPI index, private client records or checkout. A recommendation requires fresh full requested coverage at two eligible chains, with no close call or size-basis disagreement. Configured coverage does not promise current prices or every local branch."},"servers":[{"url":"https://grocerypulse.ca"}],"security":[],"externalDocs":{"description":"Scope, examples and source citation","url":"https://grocerypulse.ca/agents"},"paths":{"/api/shopper/catalog":{"get":{"operationId":"getShoppingCatalog","summary":"Discover configured cities, canonical items, quantity units and store keys","responses":{"200":{"description":"Static definitions, not current availability or prices.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/shopper/compare":{"get":{"operationId":"compareShoppingListByUrl","summary":"Read a dated comparison for an explicit city and list","description":"Read-only URL form for browsing agents. Use POST to keep the list out of URL/access logs. Responses are no-store; default limit 30 requests/minute/client on this process. Unknown, duplicate or malformed inputs are rejected.","parameters":[{"name":"city","in":"query","required":true,"schema":{"type":"string","enum":["vancouver","calgary","edmonton","saskatoon","winnipeg","toronto","ottawa","montreal","quebec-city","moncton","halifax","charlottetown","st-johns"]}},{"name":"items","in":"query","required":true,"description":"Comma-separated IDs, optionally xquantity:1,3x2,7x0.5. No implicit starter list. Quantities follow the catalogue.","schema":{"type":"string","maxLength":1200}},{"name":"stores","in":"query","required":false,"description":"Comma-separated exact chain keys. Omit to compare tracked eligible chains; an unavailable requested chain produces an error.","schema":{"type":"string"}}],"responses":{"200":{"description":"A dated comparison. Insufficient coverage is a valid answer, not a failed request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Comparison"}}}},"400":{"description":"A stable error code and catalogue link. No substitute city/list is used.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"catalogUrl":{"type":"string","format":"uri"}}}}}},"429":{"description":"A stable error code and catalogue link. No substitute city/list is used.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"catalogUrl":{"type":"string","format":"uri"}}}}}},"503":{"description":"A stable error code and catalogue link. No substitute city/list is used.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"catalogUrl":{"type":"string","format":"uri"}}}}}}}},"post":{"operationId":"compareShoppingList","summary":"Read a comparison using a bounded JSON list; no purchase or persistent state","description":"At most 8192 bytes, 50 unique canonical item IDs and 40 unique chain keys. Whole packages 1–20; per-kg quantities 0.25–10 in 0.25 kg steps. Prices at most 8 calendar days old; expired price-lowering promotions, uncertain locations and normalized-count checkout ambiguity are excluded. Lists are not stored or logged by this handler. City/date/source scope remains visible.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Request"},"example":{"city":"ottawa","items":[{"id":1,"qty":1},{"id":3,"qty":1}],"stores":[]}}}},"responses":{"200":{"description":"A dated comparison. Insufficient coverage is a valid answer, not a failed request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Comparison"}}}},"400":{"description":"A stable error code and catalogue link. No substitute city/list is used.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"catalogUrl":{"type":"string","format":"uri"}}}}}},"413":{"description":"A stable error code and catalogue link. No substitute city/list is used.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"catalogUrl":{"type":"string","format":"uri"}}}}}},"415":{"description":"A stable error code and catalogue link. No substitute city/list is used.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"catalogUrl":{"type":"string","format":"uri"}}}}}},"429":{"description":"A stable error code and catalogue link. No substitute city/list is used.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"catalogUrl":{"type":"string","format":"uri"}}}}}},"503":{"description":"A stable error code and catalogue link. No substitute city/list is used.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"catalogUrl":{"type":"string","format":"uri"}}}}}}}}}},"components":{"schemas":{"Request":{"type":"object","additionalProperties":false,"required":["city","items"],"properties":{"city":{"type":"string","enum":["vancouver","calgary","edmonton","saskatoon","winnipeg","toronto","ottawa","montreal","quebec-city","moncton","halifax","charlottetown","st-johns"]},"items":{"type":"array","minItems":1,"maxItems":50,"description":"IDs must be unique; ask the shopper before substituting a canonical item for a different variant or branded product.","items":{"type":"object","additionalProperties":false,"required":["id","qty"],"properties":{"id":{"type":"integer","enum":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50]},"qty":{"type":"number","minimum":0.25,"maximum":20,"multipleOf":0.25}},"allOf":[{"if":{"properties":{"id":{"enum":[7,8,9,11,18,21,27,50]}}},"then":{"properties":{"qty":{"type":"number","minimum":0.25,"maximum":10,"multipleOf":0.25}}},"else":{"properties":{"qty":{"type":"integer","minimum":1,"maximum":20}}}}]}},"stores":{"type":"array","maxItems":40,"uniqueItems":true,"items":{"type":"string","enum":["atlanticsuperstore","dominion","farmboy","foodbasics","foodland","fortinos","freshco","gianttiger","iga","loblaws","maxi","metro","nofrills","provigo","safeway","saveonfoods","sobeys","superc","superstore","thriftyfoods","valumart","voila","walmart","wholesaleclub","yourindependentgrocer","zehrs"]},"default":[]}}},"Comparison":{"type":"object","required":["version","currency","city","evaluatedOn","comparisonScope","recommendation","stores","requestedItemIds","comparedItemIds","untrackedItemIds","exclusions","sourceUrls","comparisonUrl"],"properties":{"version":{"type":"string","const":"1"},"currency":{"type":"string","const":"CAD"},"city":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"province":{"type":"string"}}},"evaluatedOn":{"type":"string","format":"date","description":"UTC evaluation day; observation dates can be earlier."},"comparisonScope":{"type":"string","enum":["complete_requested_list","partial_requested_list","no_comparable_items"]},"requestedItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"comparedItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"untrackedItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"weightedItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"normalizedCountItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"unsupportedQuantityItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"recommendation":{"type":"object","required":["status","storeKeys","reasons","closeStoreKeys"],"properties":{"status":{"type":"string","enum":["lowest","too_close_to_call","insufficient_coverage","size_basis_disagrees"]},"storeKeys":{"type":"array","items":{"type":"string"},"maxItems":40,"description":"One key only when status is lowest; otherwise empty. Not every local branch or a guarantee of checkout/stock."},"reasons":{"type":"array","items":{"type":"string"},"maxItems":40},"closeStoreKeys":{"type":"array","items":{"type":"string"},"maxItems":40}}},"stores":{"type":"array","maxItems":40,"items":{"type":"object","required":["key","name","totalCad","totalScope","completeRequestedList","missingItemIds","itemObservationDates"],"properties":{"key":{"type":"string"},"name":{"type":"string"},"totalCad":{"type":["number","null"],"minimum":0,"description":"CAD, rounded to cents. Read totalScope; a subtotal is not a full-list total. Null means no observed prices."},"sizeAdjustedTotalCad":{"type":["number","null"],"minimum":0,"description":"CAD, rounded to cents. Read totalScope; a subtotal is not a full-list total. Null means no observed prices."},"totalScope":{"type":"string","enum":["complete_requested_list","observed_subtotal","no_observed_prices"]},"completeRequestedList":{"type":"boolean"},"pricedItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"missingItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"oldest":{"type":["string","null"],"format":"date"},"newest":{"type":["string","null"],"format":"date"},"onSaleItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"conditionalPromotionItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"differentSizeItemIds":{"type":"array","items":{"type":"integer","minimum":1,"maximum":50},"maxItems":50},"itemObservationDates":{"type":"array","maxItems":50,"items":{"type":"object","properties":{"itemId":{"type":"integer"},"observedAt":{"type":"string","format":"date"}}}}}}},"excludedStores":{"type":"array","maxItems":40,"items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"reason":{"const":"uncertain_location"}}}},"exclusions":{"type":"array","maxItems":2000,"items":{"type":"object","properties":{"storeKey":{"type":"string"},"itemId":{"type":"integer"},"reason":{"type":"string"}}}},"observationSpan":{"type":"object","properties":{"oldest":{"type":["string","null"],"format":"date"},"newest":{"type":["string","null"],"format":"date"}}},"caveats":{"type":"array","items":{"type":"string"}},"sourceUrls":{"type":"array","maxItems":4,"items":{"type":"string","format":"uri"}},"comparisonUrl":{"type":"string","format":"uri","description":"Existing shopper tool with explicit choices; its broader dated UI can include older/uncertain-location prices. Cite the strict API evaluation when recommending."}}}}}}