---
title: "Response Schema"
description: "SearchResult, Product, ReviewPage, CategoryList and ShippingQuote — the normalized types every marketplace returns, field by field."
---

Five types cover every marketplace operation. A field means the same thing wherever it appears, and
every concept has exactly one name — a delivery is the same shape on a search card, a product, an offer
and a shipping quote.

Optional fields are genuinely optional: **absent means the marketplace did not say**, never a default.
An absent `price` is a listing with no current offer, not a free one. An absent `delivery.cost` is
shipping that has not been priced yet, not free shipping — that is `delivery.free: true`, a different
claim.

Which optional fields a given marketplace actually fills varies a lot; each marketplace page has the
measured rates.

## SearchResult

```json
{
  "marketplace": "amazon",
  "query": "running shoes",
  "page": { "index": 1, "size": 48, "total": 1200 },
  "items": [ /* SearchItem */ ],
  "cursor": "…"
}
```

| Field | Type | Notes |
|---|---|---|
| `marketplace` | string | which marketplace answered |
| `query` | string? | absent on a visual search — the anchor was a picture |
| `page.index` | number | 1-based |
| `page.size` | number | items actually returned, not the size asked for |
| `page.total` | number? | how many items the whole list holds, exactly as the marketplace states it; omitted for a rounded figure ("260,000+"). Not a promise that every one is reachable: paging ends when the next page does |
| `items` | SearchItem[] | |
| `cursor` | string? | opaque; only marketplaces that paginate by cursor emit one |
| `facets` | Facet[]? | the marketplace's own filter rail for this query — see below |

### Facet

The site's own filter rail, for this query. Absent means no rail was returned; `[]` means there is no
rail for this query.

| Field | Type | Notes |
|---|---|---|
| `id` | string | the group's key: `brand`, `sock_rise` |
| `name` | string | its label: "Brand", "Sock Rise" |
| `kind` | `select` \| `range` | `select` lists values; `range` states bounds |
| `values[]` | FacetValue | `{value, name, count?, selected?}` |
| `min` `max` | number? | for a `range`; money ranges are in the same units as `Money.amount` |
| `maps_to` | string? | the typed filter covering the same concept, e.g. `brands` |

`value` is what you send back in the `facets` request parameter. It is the human-readable label rather
than an internal id.

Where `maps_to` is present, prefer that typed parameter: it means the same thing on every marketplace.

## SearchItem and Product

Both extend one item shape, so a field means the same in a result card and on a product page. `Product`
adds what only a detail view has.

### Shared by Both

| Field | Type | Notes |
|---|---|---|
| `marketplace` `id` `title` | string | always present |
| `url` | string? | the listing's own page |
| `price` | Money? | what a buyer pays today |
| `listPrice` | Money? | the crossed-out reference it is discounted from |
| `condition` | string? | as worded: `"Pre-Owned"`, `"Used: Very Good"`, `"New"` |
| `conditionNote` | string? | the seller's own note about that condition |
| `unitPrice` | object? | `{price, unit}` — `"$1.00 / count"` |
| `brand` | string? | lifted out of the specification table; a "Brand" row is not repeated in `attributes` |
| `identifiers` | object? | `{gtin?, mpn?, model?}` — barcode (UPC/EAN/ISBN), part/model number, model name |
| `rating` | Rating? | `{average, count?, distribution?, grouped?}` |
| `seller` | Seller? | `{id?, name?, url?, rating?, country?, detailedRatings?, positivePercent?}` — see below |
| `salesCount` | number? | normalized units sold; a **floor**, since marketplaces round |
| `salesLabel` | string? | the blurb verbatim: `"3,000+ sold"` |
| `inStock` | boolean? | |
| `stock` | number? | units available, where the marketplace states a number |
| `status` | string? | the availability line as worded: `"Only 3 left in stock"` |
| `delivery` | DeliveryOption? | the default way it ships — see below |
| `shippingOptions` | DeliveryOption[]? | every option shown or quoted, the default first |
| `returnsPolicy` | string? | |
| `badges` | string[] | merchandising badges: `["Amazon's Choice"]` |
| `promotions` | string[] | savings: `["5% off coupon applied"]` |
| `paymentOptions` | string[]? | ways to pay: `["Pay in 4 payments at 0% interest"]` |
| `listingFormat` | string? | how it sells: `"Buy It Now"`, `"Auction"` |
| `groupId` | string? | the variation family this belongs to |
| `attributes` | Attribute[] | `{name, value}` pairs — the specification table, minus rows lifted into typed fields |
| `offersSummary` | object? | `{count?, fromPrice?, url?}` — the roll-up, not the list |
| `priceRange` | object? | `{min, max}` in Money — the span across the variants, as the marketplace states it ("$12.99 – $24.99"); never computed from a partial variant list |
| `endsAt` | string? | when the current offer ends: an auction's close or a time-limited deal's end. ISO 8601 |
| `purchaseLimit` | number? | most units one buyer may order of this item ("Limit 2 per customer") |
| `listedAt` | string? | when the listing was first published on the marketplace (not when we fetched it). ISO 8601 date or time |
| `deliveryTo` | object? | `{label?, country?, postalCode?}` — the place the `delivery` and `shippingOptions` promises were quoted for |
| `ext` | object? | a marketplace-only fact no shared field holds — see below |

`Money` is `{currency, amount}` with `amount` in decimal major units.

`ext` is a last resort: a fact goes into a shared typed field whenever one means the same thing, and only a
fact no other marketplace has rides in `ext`, under that marketplace's own name. Items, variants, sellers,
offers, reviews, review pages and category nodes can carry one.

### DeliveryOption

One shape for every delivery — `delivery`, each of `shippingOptions`, an offer's `delivery`, and each
option of a [ShippingQuote](#shippingquote).

| Field | Type | Notes |
|---|---|---|
| `estimate` | string? | the arrival estimate as worded: `"FREE delivery Wednesday, October 28"` |
| `cost` | Money? | the shipping fee |
| `free` | boolean? | the marketplace says shipping is free |
| `minDays` `maxDays` | number? | transit window in days, where stated as numbers |
| `handlingDays` | number? | days before the seller dispatches |
| `guaranteedDays` | number? | the marketplace's own delivery guarantee |
| `cutoff` | string? | order-by countdown for this option: `"Order within 3 hrs 22 mins"` |
| `condition` | string? | who or what qualifies: `"on orders shipped by Amazon over $35"` |
| `available` | boolean? | `false` — the marketplace does not ship to the region asked |
| `membership` | string? | the membership this option needs, e.g. `"Prime"`; absent ⇒ every buyer |
| `minimumOrder` | Money? | the order total its `cost`/`free` needs — Amazon's "on $35 of items" |
| `carrier` `code` | string? | the service, as the marketplace names it |
| `tracking` | boolean? | |
| `shipsFrom` | string? | country the parcel leaves from |

An absent `cost` is **unstated**, not free; free is `free: true`. `available: false` is an answer, never
a zero cost. `delivery` is always the option that applies to **every** buyer when the marketplace
shows one — a member-only `free` (Prime) is in `shippingOptions` with its `membership`, never presented
as everyone's.

### Seller

| Field | Type | Notes |
|---|---|---|
| `id` `name` `url` | string? | |
| `rating` | Rating? | the seller's feedback score, in the same shape as a product's `rating` |
| `country` | string? | where the seller is based |
| `detailedRatings` | object? | `{itemAsDescribed?, communication?, shippingSpeed?}` on the marketplace's 0–5 scale |
| `positivePercent` | number? | the share of the seller's feedback that is positive, as a percent from 0 to 100 ("99.8% positive"); not a star score |

`rating.grouped: true` means the aggregate covers the whole variation family rather than the one variant
asked for. Its absence means *not known to be grouped*, which is weaker than "known to be per-variant".

### SearchItem Only

| Field | Type | Notes |
|---|---|---|
| `imageUrl` | string? | the card's primary image |
| `sponsored` | boolean? | a paid placement rather than an organic result |
| `variantCount` | number? | how many combinations the card says the item comes in ("+12 colors"); `product` lists them in `variants` |
| `category` | CategoryStep[]? | where the result sits in the taxonomy, leaf last; a card usually carries only the leaf |
| `matchScore` | number? | image search only: how closely the result matches your picture, 0–1 (1 = the marketplace's best match); comparable within one result set |

### Product Only

| Field | Type | Notes |
|---|---|---|
| `description` | string? | may be HTML |
| `features` | string[] | the bulleted selling points, in page order |
| `media` | object | `{images: string[], videos: []}` |
| `category` | CategoryStep[] | full path, leaf last; a step has an `id`, a `name`, or both |
| `options` | Option[] | the axes: Colour, Size |
| `variants` | Variant[] | the combinations — see below |
| `offers` | Offer[] | competing sellers, where the marketplace enumerates them |
| `subscriptionPrice` | Money? | Subscribe & Save and equivalents |
| `reviewSummary` | string? | the marketplace's own generated digest |
| `reviewAspects` | array | labelled sentiment: `{name, mentions?, positive?, negative?}` |
| `boxContents` | string[] | "what's in the box" |
| `brandUrl` `fulfilledBy` `replacedBy` | string? | |
| `package` | object? | the shipped parcel: `{lengthCm?, widthCm?, heightCm?, weightKg?, soldInLot?}` |
| `wholesale` | boolean? | the listing offers a bulk price (see `variants[].bulkPrice`) |
| `reviews` | Review[] | top reviews carried by the product page itself |
| `included` | string[]? | which requested expansions were actually fetched |

### Variants, and the One Thing to Check

Two variation models exist in the wild, and one field tells them apart:

- **`variant.productId` present** — the combination is its own fetchable product. Pass that id back to
  `product`. Amazon works this way.
- **`variant.productId` absent** — the combination only exists inside this product; you already have
  everything. Shopify, Walmart and AliExpress work this way.

You never need to know which marketplace you are talking to. Check the field.

| Variant field | Type | Notes |
|---|---|---|
| `id` | string | the marketplace's id for the combination |
| `productId` | string? | present ⇒ fetchable with `product` (above) |
| `sku` | string? | the merchant's own stock-keeping code |
| `price` `listPrice` | Money? | |
| `bulkPrice` | Money? | per-unit price when buying in bulk, only where it differs from `price` |
| `purchaseLimit` | number? | most units one buyer may order |
| `taxIncluded` | boolean? | whether `price` includes tax |
| `inStock` | boolean? | |
| `stock` | number? | units available of this variant |
| `identifiers` | object? | `{gtin?, mpn?, model?}` — this combination's own barcode and part number, where published per variant |
| `weightKg` | number? | this combination's weight in kilograms |
| `selections` | array | `{optionId, valueId, optionName, valueName}` — which option values it is |

### Offer

A competing seller, where the marketplace enumerates them: `{seller?, price, listPrice?, condition?,
conditionNote?, fulfilledBy?, inStock?, status?, delivery?}`. `delivery` is a DeliveryOption, because a
cheaper offer that ships later is a different offer.

## ReviewPage

```json
{
  "marketplace": "walmart",
  "productId": "996425081",
  "summary": { "average": 4.6, "count": 1204, "distribution": { "5": 78 } },
  "page": { "index": 1, "size": 10, "total": 990 },
  "reviews": [ /* Review */ ],
  "grouped": true
}
```

| Field | Type | Notes |
|---|---|---|
| `summary` | Rating? | describes whatever set `reviews` was drawn from |
| `page.total` | number? | how many reviews the list holds, as the marketplace states it — can be fewer than `summary.count`, which counts every rating (Walmart lists only reviews with text). **`0` means the product has no reviews** |
| `reviews[]` | Review | `{id?, author?, rating, title?, body?, date?, country?, verified?, helpfulCount?, variant?, images[], incentivized?, reply?}` |
| `grouped` | boolean? | these are the whole family's reviews, not only this product's |
| `cursor` | string? | |

### A Product with No Reviews

It answers `200` with an explicit zero, so you can show "0 reviews" with confidence:

```json
{ "marketplace": "shopify", "productId": "shop.com/handle", "page": { "index": 1, "size": 0, "total": 0 }, "reviews": [] }
```

`page.total: 0` is only ever sent when the product genuinely has none. If there is evidence that the
product *has* reviews that are not available — a count or an average is reported, or the product page
shows them — you get an error instead, never a zero. So an empty `reviews` array tells you one of
two things, and `page.total` says which:

| `page.total` | Means |
|---|---|
| `0` | this product has no reviews |
| anything else (a number, or absent) | you have paged past the end of the reviews |

There is no `summary` on a zero answer: there is no average to report.

`grouped` is a required disclosure rather than a detail. A merchant can answer one colour's page with
the family's 9,000 reviews and nothing in the reviews themselves says so — computing "this product's
rating" from a grouped page gives you the family's.

`review.incentivized: true` marks a review written in exchange for something (a free product, a sample, a
reward), as the marketplace labels it; absent means not marked, never "known to be unpaid". `review.reply` is
the merchant's public response: `{body, date?, author?}`.

`review.variant` names the sibling a review was written for, when the marketplace says. Inside a grouped
page an empty `variant` means *the marketplace would not say*, never "this one is for the product you
asked for".

`page.total` and `summary.count` count different things: a product can report 7,654 ratings while its review
list holds 990 written reviews, so the count stays true to the product and the total to the list. Neither
promises that every review is reachable: marketplaces cap how far a list pages, and the list ends when the
`cursor` does.

## CategoryList

What `categories` returns: one flat list, every node naming its parent. Flat rather than nested because
the common request is "the whole taxonomy", and nesting makes that both larger on the wire and harder
to page. Rebuilding the tree is a `groupBy` on `parentId`.

```json
{
  "marketplace": "aliexpress",
  "path": [ { "id": "200000345", "name": "Women's Clothing" } ],
  "categories": [
    { "id": "349", "name": "Skirts", "parentId": "200000345" },
    { "id": "201531101", "name": "Matching Sets", "parentId": "200000345" }
  ]
}
```

| Field | Type | Notes |
|---|---|---|
| `marketplace` | string | |
| `path` | CategoryStep[] | Ancestors of the category asked for, leaf last. Empty at the root. |
| `categories` | CategoryNode[] | |

### CategoryNode

| Field | Type | Notes |
|---|---|---|
| `id` | string | **The id you pass back** — to `categories` to descend, and to `browse` when `browsable` allows |
| `name` | string | |
| `parentId` | string? | Absent at a root |
| `hasChildren` | boolean? | Absent means **unknown**, not "no" — most marketplaces do not say |
| `browsable` | boolean? | See below |
| `productCount` | number? | Only when the marketplace states one; never counted from a page |

Unlike most of this schema, `id` and `name` are **both required**. A category listing exists to be
acted on: an entry without an id is unusable and one without a name unreadable.

### `browsable` — the One Field Worth Reading Carefully

**Absent means yes**, which is the normal case. `false` marks a level that is real and navigable but
has **no listing page behind it**.

Every marketplace here has such levels, and they are not an edge case:

| | What is not browsable |
|---|---|
| Amazon | departments (they are store slugs, not browse nodes) |
| Walmart | some departments — Electronics lists products, Toys is a hub with no grid |

When you meet one, **descend one more level with `categories`** and the ids there will browse.

Pass **`browsableOnly=true`** to `categories` to get only the ids `browse` accepts — see
[the marketplaces overview](/docs/api-reference/marketplaces) for what it costs and when it 400s.

### You Do Not Have to Trust the Flag

`browsable` is populated only where it has been established, so a level that cannot be browsed can
still reach you with the field absent — that is, optimistically. On Walmart this is unavoidable:
Electronics (`3944`) and Toys (`4171`) are both departments with children, and one lists products while
the other does not, with nothing in the taxonomy to tell them apart.

So **`browse` tells you outright**. Asking it for a category that has no listing page returns

```json
{ "success": false, "error": { "code": "CATEGORY_NOT_BROWSABLE", "message": "Walmart category 4171 is a navigation hub…" } }
```

rather than an empty list — because an empty list is also what a sold-out category returns, and you
would have no way to tell "descend to the children" from "nothing for sale here". Treat the code as
the instruction to call `categories` with that same id and browse one of its children. It is a 400,
not a failure of the request: retrying will not change the answer.

## ShippingQuote

What [`shipping`](/docs/api-reference/aliexpress/shipping) returns: every way one item ships to one
region, cheapest first.

```json
{
  "marketplace": "aliexpress",
  "productId": "1005012143419303",
  "skuId": "12000057632572864",
  "region": "US",
  "quantity": 1,
  "available": true,
  "options": [
    { "carrier": "YunExpress Standard Shipping", "cost": { "currency": "USD", "amount": 3.13 },
      "free": false, "minDays": 7, "maxDays": 15, "guaranteedDays": 60,
      "estimate": "Oct. 30 - Nov. 07", "tracking": true, "shipsFrom": "CN" }
  ]
}
```

`options` are [DeliveryOption](#deliveryoption)s. `available: false` with no options means the
marketplace does not ship there — an answer, not an error.

## Errors

```json
{ "success": false, "error": { "code": "INVALID_PARAMS", "message": "…" } }
```

| Status | Code | Means |
|---|---|---|
| 400 | `INVALID_PARAMS` | a parameter is missing, malformed, or not one this marketplace accepts |
| 400 | `CATEGORY_NOT_BROWSABLE` | the category exists but has no listing page — descend with `categories` and browse a child |
| 404 | `NOT_FOUND` | that id does not exist |
| 404 | `UNKNOWN_MARKETPLACE` / `UNKNOWN_OPERATION` | check `GET /v1/marketplaces` |
| 429 | rate limited | carries `Retry-After` |
| 501 | `UNSUPPORTED` | the data exists but is not available through this API — not a problem with your request |
| 502 | upstream | the marketplace was unavailable; retry |

A 501 is never a 404 on purpose: "these reviews are not available" must not read as "this product has
no reviews".
