# Electronics Catalog API Read-only catalogue of **mobiles and laptops sold in India** (Tamil Nadu focus), available as a REST API and as an MCP server for AI assistants. Every product is verified by real listings on **at least two retail platforms**, and every price, image, rating and review comes with the page it was read from. Nothing is generated. | | | |---|---| | **Base URL** | `http://31.97.228.132:8000` (HTTPS address coming: `https://catalogue-api.workolik.com`) | | **Interactive docs** | `http://31.97.228.132:8000/docs` (try every endpoint in the browser) | | **OpenAPI schema** | `http://31.97.228.132:8000/openapi.json` | | **MCP endpoint** | `http://31.97.228.132:8000/mcp/` (keep the trailing slash) | | **Auth** | None for everything in this document | | **Format** | JSON, UTF-8 | Current data: **43 verified products** (15 mobiles, 28 laptops) from Amazon, Flipkart, Croma, Reliance Digital, Vijay Sales, Tata CLiQ, Poorvika, Sangeetha, Vasanth & Co and Viveks. --- ## Conventions - **Money is a decimal string in rupees**: `"42999.00"`, never a float. Parse it as a decimal. - **Times are ISO 8601** with timezone: `"2026-09-29T12:54:38.447374+05:30"`. - **`null` means "not stated by any source"**, not zero. For example `rating: null` means no platform publishes a rating. - **Errors** return an HTTP status with `{"detail": "..."}`. Unknown product → `404`. Bad parameter → `422`. - **Data freshness**: the catalogue is refreshed by collection runs, not live per request. Each offer has `observed_at`, the time it was last read. --- ## Endpoints | Method | Path | Returns | |---|---|---| | GET | [`/api/health`](#get-apihealth) | Service and database status | | GET | [`/api/elec/categories`](#get-apieleccategories) | Categories with product counts | | GET | [`/api/elec/brands`](#get-apielecbrands) | Brands in a category | | GET | [`/api/elec/products`](#get-apielecproducts) | Product list with filters | | GET | [`/api/elec/products/{product_id}`](#get-apielecproductsproduct_id) | Full product detail | | GET | [`/api/elec/products/{product_id}/price-history`](#get-apielecproductsproduct_idprice-history) | Price over time per platform | | GET | [`/api/elec/sites`](#get-apielecsites) | The retail platforms read | ### `GET /api/health` ```bash curl http://31.97.228.132:8000/api/health ``` ```json { "status": "ok", "database": true, "database_name": "loyalycatalogue", ... } ``` `status` is `"degraded"` when the database is unreachable. ### `GET /api/elec/categories` ```bash curl http://31.97.228.132:8000/api/elec/categories ``` ```json [ { "slug": "laptops", "name": "Laptops", "product_count": 28 }, { "slug": "mobiles", "name": "Mobiles", "product_count": 15 } ] ``` ### `GET /api/elec/brands` | Param | Required | Example | |---|---|---| | `category` | yes | `laptops` | ```bash curl "http://31.97.228.132:8000/api/elec/brands?category=laptops" ``` Each item: `brand`, `brand_slug` (use it to filter products), `product_count`, `min_price`, `max_price`, `sample_image`. Brands with `product_count: 0` are allow-listed but have no verified products yet. ### `GET /api/elec/products` | Param | Example | Meaning | |---|---|---| | `category` | `mobiles`, `laptops` | Category slug | | `brand` | `samsung`, `hp` | Brand slug from `/brands` | | `q` | `galaxy a56` | Text search on product and brand name (max 100 chars) | | `min_price`, `max_price` | `30000` | Filter on `best_price`, rupees | | `site` | `croma.com` | Only products listed on that platform (domain from `/sites`) | | `limit` | `48` | 1–200, default 48 | | `offset` | `0` | Paging offset | Sorted by number of platforms (most first), then price. ```bash curl "http://31.97.228.132:8000/api/elec/products?category=mobiles&brand=samsung&limit=1" ``` ```json { "total": 6, "products": [ { "product_id": 551, "brand": "Samsung", "category": "mobiles", "display_name": "Samsung Galaxy A56 (8GB RAM, 256GB)", "ram_gb": 8.0, "storage_gb": 256.0, "best_price": "42999.00", "best_price_site": "Reliance Digital", "platform_count": 5, "image_url": "https://img-prd-pim.poorvika.com/product/Samsung-Galaxy-A56-5G-Awesome-Graphite-Main.png" } ] } ``` `total` is the full match count; use `limit`/`offset` to page. Some extra fields (`brand_slug`, `family`, `model`, `processor`, `sold_by_tn_retailer`, `updated_at`, ...) are also present. ### `GET /api/elec/products/{product_id}` ```bash curl http://31.97.228.132:8000/api/elec/products/551 ``` Everything from the list item, plus: | Field | Content | |---|---| | `offers[]` | One per platform listing: `site`, `domain`, `site_region` (`TN` / `national`), `price`, `mrp`, `in_stock`, `source_url`, `source_type`, `listing_title`, `observed_at`, `price_outlier` | | `canonical_specs` | Normalised specs, e.g. `ram_gb`, `storage_gb`, `display_inch`, `processor`, `battery_mah`, `os` | | `spec_sources` | Per spec, the page it was read from | | `images[]` | `url`, `site`, `found_on` (page the image is on), `source_type` | | `rating` | `{ value, count, sources: [{ site, rating, review_count, source_url }] }`, or `null` | | `reviews[]` | Up to 10 real customer reviews: `site`, `source_url`, `author`, `rating`, `title`, `body`, `review_date`, `sentiment` (`positive` / `neutral` / `negative`, from the reviewer's own stars) | One offer from the response: ```json { "site": "Reliance Digital", "domain": "reliancedigital.in", "site_region": "national", "price": "42999.00", "mrp": null, "in_stock": true, "source_url": "https://www.reliancedigital.in/product/samsung-galaxy-a56-5g-256-gb-8-gb-ram-awesome-olive-mobile-phone-m7x9g6-8968988", "source_type": "scraped_page", "observed_at": "2026-09-29T12:54:38.447374+05:30", "price_outlier": false } ``` Notes: - **`source_type`** says how a value was read: `scraped_page` (the platform's product page), `brand_official` (the brand's site) or `search_snippet` (a search-engine result; Amazon and Flipkart are only read this way). - **`price_outlier: true`** marks a search-engine price that disagrees with the product-page prices. It is kept for transparency but never used as `best_price`. - **`in_stock`** is `true`, `false` or `null` (not stated). `best_price` prefers in-stock offers, but a product that is out of stock everywhere still shows its price. - **Reviews are often empty**: most retailers do not publish review text in a readable form, and none are invented. ### `GET /api/elec/products/{product_id}/price-history` ```bash curl http://31.97.228.132:8000/api/elec/products/551/price-history ``` ```json [ { "site": "Reliance Digital", "price": "42999.00", "mrp": null, "in_stock": true, "source_type": "scraped_page", "observed_at": "2026-09-29T12:54:38.447374+05:30" } ] ``` Oldest first, one row per observation per platform. ### `GET /api/elec/sites` ```bash curl http://31.97.228.132:8000/api/elec/sites ``` Each platform: `name`, `domain`, `kind` (`marketplace`, `national_chain`, `tn_regional`, `brand_official`), `region`, and how many listings were read from it. --- ## Using it from code **Python (`requests`)** ```python import requests from decimal import Decimal BASE = "http://31.97.228.132:8000" r = requests.get(f"{BASE}/api/elec/products", params={"category": "laptops", "max_price": 60000, "limit": 100}, timeout=30) r.raise_for_status() for p in r.json()["products"]: print(p["display_name"], Decimal(p["best_price"]), "at", p["best_price_site"]) detail = requests.get(f"{BASE}/api/elec/products/551", timeout=30).json() for offer in detail["offers"]: print(offer["site"], offer["price"], offer["source_url"]) ``` **JavaScript (`fetch`)** ```js const BASE = "http://31.97.228.132:8000"; const res = await fetch(`${BASE}/api/elec/products?category=mobiles&q=galaxy`); const { total, products } = await res.json(); products.forEach(p => console.log(p.display_name, p.best_price, p.best_price_site)); ``` Browsers only allow calls from the web apps on the API's allowlist (`app.nearledaily.com`, `catalogue.nearle.ai.in`, `localhost:3100`). Calls from servers, scripts, curl and MCP clients are not affected. Ask for a new web origin to be added if needed. --- ## MCP (for AI assistants) The same catalogue is an MCP server (Streamable HTTP transport) at **`http://31.97.228.132:8000/mcp/`**. Read-only, no login. | Tool | Arguments | Returns | |---|---|---| | `list_categories` | none | Categories with product counts | | `search_products` | `query`, `category`, `brand`, `max_price`, `min_price`, `limit` (all optional, `limit` 1–100, default 20) | `total` and products: id, name, RAM/storage, best price and platform, platform count, image URL | | `get_product` | `product_id` | Offers per platform, specs, `image_urls`, rating, reviews | | `price_history` | `product_id` | Every observed price per platform | Images are returned as **URLs** on the retailers' own image servers; nothing is re-hosted. Only part of the catalogue has an image. **Claude Code** ```bash claude mcp add --transport http electronics-catalog http://31.97.228.132:8000/mcp/ ``` **Cursor** (`.cursor/mcp.json`) and other clients that take a URL ```json { "mcpServers": { "electronics-catalog": { "url": "http://31.97.228.132:8000/mcp/" } } } ``` **Python (`fastmcp`)** ```python import asyncio from fastmcp import Client async def main(): async with Client("http://31.97.228.132:8000/mcp/") as c: found = await c.call_tool("search_products", {"category": "laptops", "max_price": 60000}) print(found.data["total"]) detail = await c.call_tool("get_product", {"product_id": 551}) print(detail.data["display_name"], detail.data["image_urls"]) asyncio.run(main()) ``` Example questions once connected: *"Which laptops under ₹60,000 are sold on the most platforms?"*, *"Compare Galaxy A56 prices across stores"*, *"Show the price history of product 551."* --- ## Not part of this API `/api/auth/*` and `/api/elec/admin/*` (collection runs, platform probes) need an admin login and are for the catalogue operators only.