Verified catalogue of mobiles and laptops sold in India, collected from real retail listings (FastAPI backend, React frontend, Postgres/pgvector). - REST API under /api/elec (read-only catalogue; admin endpoints need login) - MCP server (FastMCP) at /mcp/ with list_categories, search_products, get_product and price_history tools - Real ratings and reviews read from product pages and search results - Production Dockerfile (requirements-api.txt, no PyTorch) and .env.production.example; remote database only via an explicit ELEC_ALLOW_REMOTE_DB host/name allowlist - docs/API.md: endpoint and MCP reference with live examples Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
253 lines
9.9 KiB
Markdown
253 lines
9.9 KiB
Markdown
# 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.
|