Response Schema
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
{
"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.
| 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.productIdpresent — the combination is its own fetchable product. Pass that id back toproduct. Amazon works this way.variant.productIdabsent — 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
{
"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:
{ "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.
{
"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 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
{ "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 returns: every way one item ships to one
region, cheapest first.
{
"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 DeliveryOptions. available: false with no options means the
marketplace does not ship there — an answer, not an error.
Errors
{ "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".