## What it is

A Product row is a capture of one retailer's listing at the moment
Stylumia's crawler saw it — not a normalized master product, and not a
merged view across retailers. Two retailers selling what is obviously the
same physical item produce two independent rows, with no built-in link
between them.

## The identity contract

Two identifiers matter here, and they mean different things:

- **`sku`** is the retailer's own identifier for the listing. It's unique
  within a retailer, and only within that retailer — the same string can
  mean something else at a different retailer.
- **`product_id`** is a Stylumia content hash (`prd_` followed by 24 hex
  characters). It identifies a listing by its content, not by a stable
  database row: if a retailer edits a listing's title, the next crawl mints
  a **new** `product_id` — the old row isn't deleted, it persists alongside
  the new one. Resolving which candidate is current means preferring the
  one with non-empty normalized attributes, then falling back to the most
  recent `crawl_date`.

This surface returns both on every row. `sku` is the identifier you'll
[filter and page by](/docs/pagination) here. The `product_id` behavior above
is documented because it's the reason the same physical listing can appear
to change identity between crawls; worth knowing before you build anything
that assumes a stable per-listing id over time.

## Representations

The row this API returns is lean — the 17 fields in the
[data dictionary](/docs/data-dictionary): `product_id`, `sku`, `url`,
`domain`, `retailer`, `department`, `category`, `brand`, `model_number`,
`title`, `mrp`, `selling_price`, `discount_pct`, `currency`, `color`,
`image_url`, `crawl_date`. There is no separate "full detail" call on this
generic surface — what you see in [`/v1/catalog` and `/v1/sample`](/reference)
is everything there is.

Price is three fields, not one: `mrp` is the list price, `selling_price` is
what was actually charged at capture time, and `discount_pct` is derived
from the two. There is no single `price` field.

## Field coverage

Not every field is populated for every row. Published up front, so you can
design around it instead of discovering it in production:

| Field group | Coverage |
|---|---|
| Ratings | ~46% of rows |
| Seller | ~68% of rows |
| Shipment | ~1% of rows — effectively absent |
| Variants | Not available on this surface |

Read the table before you integrate and you can design around the gaps.
Discover them in production and you're debugging null fields instead — or
opening a [support ticket](/docs/support) about them.

## `crawl_date` semantics

`crawl_date` is when Stylumia's crawler captured the row — not when the
retailer last changed the listing. A row with an old `crawl_date` may still
describe the retailer's current listing; it just means we haven't
recaptured it recently. `crawl_date` is also what `start_date`/`end_date`
[filter on](/docs/pagination) on every endpoint. Cadence and window depth
per retailer live on [Coverage & freshness](/docs/coverage-freshness) — a
stub today.

## Example row

```json
{
  "product_id": "prd_5741354fa5d111f0a1ce7bcc",
  "sku": "1003097550",
  "url": "https://www.homedepot.com/p/1003097550",
  "domain": "homedepot.com",
  "retailer": "home_depot",
  "department": "tools",
  "category": "power_tools",
  "brand": "DEWALT",
  "model_number": "DCD771C2",
  "title": "20V MAX Cordless Drill/Driver Kit",
  "mrp": 179.0,
  "selling_price": 139.0,
  "discount_pct": 22.3,
  "currency": "USD",
  "color": "Yellow",
  "image_url": "https://images.example.com/1003097550.jpg",
  "crawl_date": "2026-06-14"
}
```
