Files
loyaly-catalogue/docs/API.md
sriram c7e4d59188 Electronics Catalog: API, MCP server, frontend and deployment
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>
2026-10-01 12:17:42 +05:30

253 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.