Data API · v1

Muwazin Data API

Saudi grocery prices as JSON: the catalog behind Muwazin, 96,000+ products with a current price at 21 chains, matched across retailers and priced at 484 branches, with promotions, stock and history, refreshed every night. Base URL https://api.muwazin.me/v1.

Quickstart: five calls

  1. Get a key. Request access and we email you an Evaluation key, usually within two business days. Evaluation keys are for a proof of concept: 500 units and 200 products of browse for the life of the key (they run for 14 days), 30 requests a minute, 30 days of history. Developer is SAR 299 a month, Pro SAR 899 and Business SAR 2,499; Enterprise is priced on request (plans). Once you have a key, the console shows its usage and lets you rotate it.
  2. Check the key works.
curl https://api.muwazin.me/v1/me \
  -H "Authorization: Bearer mw_live_YOUR_KEY"
  1. Look a product up by barcode. Zero-padding variants are handled for you.
curl https://api.muwazin.me/v1/barcodes/6281007001081 \
  -H "Authorization: Bearer mw_live_YOUR_KEY"
  1. Search in Arabic or English. Filters: category, retailer, brand, inStock; sort by price_asc, price_desc, newest.
curl "https://api.muwazin.me/v1/products?q=حليب%20المراعي&inStock=true&sort=price_asc&limit=20" \
  -H "Authorization: Bearer mw_live_YOUR_KEY"
  1. See what moved, last night or week over week, and chart a product's history. /v1/retailers lists the chains with their price source and branch count, /v1/categories the aisles with product counts, and /v1/stores the store locations. Products without an aisle are counted as uncategorized: their category is null, and category=uncategorized finds them.
curl "https://api.muwazin.me/v1/price-changes?direction=drop&minPct=10&limit=50" \
  -H "Authorization: Bearer mw_live_YOUR_KEY"

curl "https://api.muwazin.me/v1/weekly-changes?direction=drop&minPct=10&limit=50" \
  -H "Authorization: Bearer mw_live_YOUR_KEY"

curl "https://api.muwazin.me/v1/products/PRODUCT_ID/history?from=2026-06-01&retailer=panda" \
  -H "Authorization: Bearer mw_live_YOUR_KEY"

A product, as returned

{
  "id": "m3DnxmCZCZ9j4uojubqe",
  "barcode": "6281007001081",
  "name": { "ar": "حليب المراعي كامل الدسم 2 لتر", "en": "Almarai Full Fat Milk 2L" },
  "brand": "Almarai",
  "size": { "weight": "2L", "unit": "L" },
  "priceBasis": "item",
  "category": "dairy-eggs",
  "image": "https://…",
  "matchConfidence": 0.98,
  "cheapest": { "retailer": "panda", "price": 8.95, "priceBasis": "item" },
  "prices": [
    { "retailer": "panda", "retailerName": { "ar": "بنده", "en": "Panda" },
      "price": 8.95, "priceBasis": "item", "currency": "SAR", "inStock": true, "stale": false,
      "scrapedAt": "2026-09-17T02:14:00Z", "url": "https://panda.com.sa/…" }
  ],
  "updatedAt": "2026-09-17T02:40:11Z"
}

Every field is documented in the API reference, where you can paste your key and try the calls from the page.

Branch prices

Where a retailer prices by branch, GET /v1/products/{id}/branches returns the product's price at every branch we collect (1 unit): the branch's name, city and location, the price and the branch's "was" price when it is on sale there, stock state and quantity where the retailer publishes them, its promotion and purchase limit, and stale for a branch not swept in the last eight days. A branch with no complete sweep for three days reports inStock: false. Filter with city= (any common spelling), retailer=, inStock=, or near=lat,lng&radiusKm= for the nearest real stores. store.kind is store, centre (a Lulu store whose location we do not hold: no distance) or area (a Carrefour catalogue area, no distance). Every product also carries branches.total / branches.inStock.

curl "https://api.muwazin.me/v1/products/PRODUCT_ID/branches?city=Riyadh&inStock=true" \
  -H "Authorization: Bearer mw_live_YOUR_KEY"

{ "productId": "PRODUCT_ID", "priceBasis": "item", "city": "Riyadh",
  "items": [
    { "retailer": "danube", "retailerName": { "ar": "الدانوب", "en": "Danube" },
      "store": { "code": "202", "name": { "ar": "…", "en": "Danube Al Rawdah" }, "city": "Riyadh",
                 "lat": 24.71, "lng": 46.67, "distanceKm": null, "kind": "store" },
      "price": 15.99, "priceBasis": "item", "originalPrice": 22.95, "currency": "SAR", "inStock": true,
      "stockState": "in", "stockQty": null, "promo": null, "limits": null, "flags": { "on_sale": true },
      "updatedAt": "2026-09-25T02:40:11Z", "branchLastSeen": "2026-09-25T02:40:11Z", "stale": false }
  ],
  "nextCursor": null, "dataAsOf": "2026-09-25T02:40:11Z" }

Intelligence (Business and above)

Two nightly-computed market reports are available to plans with intelligence access (5 units per call):

  • GET /v1/intel/benchmark — retailer competitiveness on the products two or more retailers stock: cheapest-price wins, win % and average/median premium per retailer.
  • GET /v1/intel/volatility?retailer=&category= — the most-changed prices of the last 30 days per product×retailer, with change counts, price ranges and a volatility score.

Authentication

Send your key on every request as Authorization: Bearer mw_live_…. The X-API-Key header is accepted too. Keys are secrets: keep them server-side, never in a mobile app or browser bundle. If a key leaks, rotate it in the console: the old secret stops working immediately, and the key keeps its plan, its expiry date and the units it has used.

Only /v1/health, these pages and the OpenAPI file are public.

Accepting the terms

A new key can call /v1/me and nothing else until the account behind it has accepted the terms of use. Every data endpoint answers 403 terms_required before that. Accept them once, either by signing in to the console or with the key itself:

curl -X POST https://api.muwazin.me/v1/terms \
  -H "Authorization: Bearer mw_live_…" \
  -H "Content-Type: application/json" \
  -d '{"version": "<the version on the terms page>", "accept": true}'

The version is printed on the terms page, in the 403 body (which also carries this exact call under terms.accept) and in /v1/me. Sending it means you read that version. Accepting costs no units and covers every key on your account; when the terms change, the new version has to be accepted again.

Prices, staleness and units

Where a price comes from

Every price carries the retailer it was collected from, the collection time scrapedAt (the last complete nightly sweep that confirmed it), the retailer's product page url, and a stale flag. Prices are collected once a night from each retailer's own website or app, or from the delivery storefront it sells through; /v1/retailers says which (priceSource) and how many branches each chain has. A price is stale when its retailer has not been collected successfully for two nights; stale and out-of-stock prices are excluded from cheapest. After three days without a complete collection, the retailer's prices also report inStock: false, as they do in the app. An item that a retailer's full nightly collection stops listing reports inStock: false from the second night it is missing (for the few retailers whose website shows part of their range each night, after 14 days). The X-Data-As-Of response header tells you when the last pipeline night finished.

Per item or per kilogram

Every price says what it buys in its own priceBasis. item is the price of one item or pack, as the retailer lists it. kg is the price of one kilogram of a product sold by weight (loose produce, meat and fish, deli cheese and olives, where the shopper picks the weight).

A product has a priceBasis too: kg when it is sold by weight. Such a product has no pack size, so its size is empty. Most of its prices are per kilogram, but a retailer that sells the same product as a pack keeps its pack price, marked item. Compare two prices only when their priceBasis is the same. A deli item sold in fixed portions can come as item throughout, at the portion's price, so read the field rather than inferring it from the category.

The field is on each entry of prices, on cheapest, on every row of /products/{id}/branches, /price-changes and /weekly-changes. /history returns priceBasisByRetailer: a retailer's whole series is in the basis of its current price.

Matching

Products are matched across retailers by barcode where retailers publish one (Carrefour and the chains sold through HungerStation do not), otherwise by name, size and brand with automated gates and manual review; a product's own barcode is checked and repaired from its listings. matchConfidence is 0–1; treat prices under a low-confidence product with care and use the url to verify. Branch prices join a product through the same listings, so a branch row always belongs to the chain's listing you see under prices.

When two products turn out to be one

Now and then two product records are found to be the same item and are merged. The id that stays keeps every price; the other id keeps working: /products/{id}, /history and /branches answer with the product it went into, under that product's id, and add mergedFrom with the id you asked for. Store the new id when you see it. A merged id is never returned by search, browse or a barcode lookup.

Units

Plans are metered in units, not calls. Most calls cost 1 unit, /products/{id}/branches included; /products (search or browse), /history, /price-changes and /weekly-changes cost 2; the /intel/* reports cost 5. Failed calls and /me cost nothing. Three headers come back on every response: X-RateLimit-Limit, X-RateLimit-Remaining and X-Quota-Remaining. The usage page in the console plots what you have spent, day by day.

Pagination

List endpoints return nextCursor. Pass it back as ?cursor= to get the next page; there is no offset paging. Search (q=) returns up to 50 per page and stops at 500 results per query, so narrow with filters. Browse (no q=) returns up to 200 per page. /stores pages the same way.

The browse allowance

Browse is also metered in products, separately from units: 200 on Evaluation, 10,000 a month on Developer, the whole catalog on Pro and above. The allowance covers the same period as your units, either the calendar month or the whole life of a key that expires, and changing the filters does not start a new one. When it runs out the last page carries "capped": true and further browse calls answer 403 browse_capped. Search, barcode and product lookups are not affected, and /me reports what is left.

Errors

Errors are JSON with a stable code:

{ "error": { "code": "rate_limited", "message": "Rate limit is 30 requests/minute for this key." } }
HTTPcodeWhat to do
401missing_key / invalid_keySend a valid key in the Authorization header.
403key_pausedWe have paused the key. Contact us at hello@muwazin.me, or wait for our email with further information. A paused key cannot be rotated or revoked in the console.
403key_revoked / key_expiredCheck the key in the console, or ask us for a new one.
403terms_requiredThe account has not accepted the current terms. POST /v1/terms with the version and accept: true (see Accepting the terms), or accept in the console.
409terms_outdatedYou sent an older terms version. Read the current one (the 409 names it) and send that.
403plan_required / browse_cappedThe browse allowance is spent, or the endpoint needs a bigger plan (the /intel/* reports need Business or above).
429rate_limited / search_rate_limitedBack off for the Retry-After seconds.
429quota_exceededMonthly units used up; resets on the 1st. Request a bigger plan.
400invalid_cursor / invalid_barcode / invalid_range / unknown_city / invalid_nearFix the request: send nextCursor back exactly as you received it, from and to are dates from 1900-01-01 to 2100-01-01, city= takes any common spelling, and near= is lat,lng in degrees.
400invalid_requestThe path or a parameter contains a NUL character (%00). Remove it.
422invalid_requestA parameter has the wrong type or is out of range. message names it, and fields lists each one (example below).
404not_foundNo such product, barcode or endpoint.
405method_not_allowedThe endpoint takes another HTTP method; the Allow header names it.
503search_unavailableSearch backend hiccup; retry shortly.

A 422 names each wrong parameter in message, and again one by one in fields (in is path, query or body):

{ "error": { "code": "invalid_request",
             "message": "`limit`: Input should be less than or equal to 200",
             "fields": [ { "in": "query", "name": "limit", "message": "Input should be less than or equal to 200" } ] } }

Plans and limits

Developer is SAR 299 a month, Pro SAR 899 and Business SAR 2,499. You pay in advance on the console's plan page, one or twelve months at a time, and nothing renews by itself: the key runs until the months you bought end, then answers key_expired until you buy more. A pass for a higher plan starts at once, and the time left on your current plan is added at its value. Enterprise is quoted on request: reply to the email that carried your key, or use the request form.

EvaluationDeveloperProBusiness
PriceFreeSAR 299 / monthSAR 899 / monthSAR 2,499 / month
Units500 per key50,000 / month250,000 / month1,000,000 / month
Requests per minute3060150300
Searches per minute10, keyword20, keyword60, hybrid120, hybrid
Price history30 days90 daysfullfull
Browse allowance200 products per key10,000 products / monthfull catalogfull catalog
Exports (CSV, by arrangement)nonoon requeston request
Benchmark & volatility reports (/intel/*)nononoyes
Attribution when displayedrequiredrequiredrequiredoptional
Supportdocsemailemail, 2 business dayspriority
Key expiry14 dayswhen your pass endswhen your pass endswhen your pass ends

Enterprise: custom volumes, SLA, contract, annual licences for institutions. Tell us what you need.

Every plan is used under the terms of use. In short: an evaluation key is for evaluation and carries no commercial licence, and the data may not be resold or republished without our written permission. A key returns no data until they are accepted, in the console or with POST /v1/terms.

Early access: there is no uptime guarantee yet, and prices are the retailers' online prices collected nightly, not prices read off physical shelves. Quotas are hard limits; when a key runs out it returns quota_exceeded until the next month, or, for an evaluation key, until we issue you a new one.
Showing Muwazin prices inside your own product is fine on every paid plan. Developer and Pro keys show a visible "Prices by Muwazin" credit linking to muwazin.me; Business keys may leave it out.