Response Schema
SearchResult、Product、ReviewPage、CategoryList 与 ShippingQuote——所有平台统一返回的归一化类型,逐字段说明。
五种类型覆盖了所有平台操作。同一个字段无论出现在哪里,含义都相同,而且每个概念只有一个名字——配送信息 在搜索卡片、商品、报价和运费报价中都是同一种形状。
可选字段是真正可选的:缺失表示平台没有给出该信息,而不是某个默认值。没有 price 意味着这条商品当前
没有在售报价,而不是免费。没有 delivery.cost 意味着运费尚未算出,而不是包邮——包邮是
delivery.free: true,那是另一个断言。
某个平台实际会填充哪些可选字段,差异很大;每个平台的文档里都有实测比例。
SearchResult
{
"marketplace": "amazon",
"query": "running shoes",
"page": { "index": 1, "size": 48, "total": 1200 },
"items": [ /* SearchItem */ ],
"cursor": "…"
}| 字段 | 类型 | 说明 |
|---|---|---|
marketplace | string | 由哪个平台作答 |
query | string? | 以图搜图时没有——依据的是一张图片 |
page.index | number | 从 1 开始 |
page.size | number | 实际返回的条数,而非请求的条数 |
page.total | number? | 整个列表的条目总数,与平台给出的确切数值一致;若平台只给出约数("260,000+")则省略。它不保证每一条都能取到:没有下一页时翻页即结束 |
items | SearchItem[] | |
cursor | string? | 不透明值;只有按游标翻页的平台才会给出 |
facets | Facet[]? | 平台针对本次查询给出的筛选栏——见下 |
Facet
站点针对本次查询给出的筛选栏。缺失表示没有返回筛选栏;[] 表示本次查询没有筛选栏。
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 该分组的键:brand、sock_rise |
name | string | 它的标签:"Brand"、"Sock Rise" |
kind | select | range | select 列出取值;range 给出上下界 |
values[] | FacetValue | {value, name, count?, selected?} |
min max | number? | 用于 range;金额区间的单位与 Money.amount 相同 |
maps_to | string? | 覆盖同一概念的类型化过滤器,例如 brands |
value 就是你在 facets 请求参数里回传的内容。它是可读的标签,而不是内部 id。
当 maps_to 存在时,优先使用那个类型化参数:它在每个平台上含义相同。
SearchItem 与 Product
两者都扩展自同一种条目结构,因此同一个字段在结果卡片和商品详情页里含义一致。Product 只是补上了详情页
才有的内容。
两者共有
| 字段 | 类型 | 说明 |
|---|---|---|
marketplace id title | string | 始终存在 |
url | string? | 该商品自身的页面 |
price | Money? | 买家今天支付的金额 |
listPrice | Money? | 被划掉的参考价,折扣由此计算 |
condition | string? | 按原文表述:"Pre-Owned"、"Used: Very Good"、"New" |
conditionNote | string? | 卖家对该成色的自述 |
unitPrice | object? | {price, unit}——"$1.00 / count" |
brand | string? | 从规格表中提取;"Brand" 行不会在 attributes 中重复出现 |
identifiers | object? | {gtin?, mpn?, model?}——条码(UPC/EAN/ISBN)、零件/型号编号、型号名称 |
rating | Rating? | {average, count?, distribution?, grouped?} |
seller | Seller? | {id?, name?, url?, rating?, country?, detailedRatings?, positivePercent?}——见下 |
salesCount | number? | 归一化后的销量;由于平台会取整,这是一个下限 |
salesLabel | string? | 原样的销量文案:"3,000+ sold" |
inStock | boolean? | |
stock | number? | 可售数量,平台给出数字时才有 |
status | string? | 按原文表述的库存说明:"Only 3 left in stock" |
delivery | DeliveryOption? | 默认的配送方式——见下 |
shippingOptions | DeliveryOption[]? | 展示或报价的全部配送选项,默认选项在前 |
returnsPolicy | string? | |
badges | string[] | 营销徽章:["Amazon's Choice"] |
promotions | string[] | 优惠:["5% off coupon applied"] |
paymentOptions | string[]? | 付款方式:["Pay in 4 payments at 0% interest"] |
listingFormat | string? | 销售方式:"Buy It Now"、"Auction" |
groupId | string? | 它所属的变体系列 |
attributes | Attribute[] | {name, value} 键值对——规格表,去掉已提取到类型化字段的行 |
offersSummary | object? | {count?, fromPrice?, url?}——汇总,而不是列表 |
priceRange | object? | 以 Money 表示的 {min, max}——平台给出的各变体价格区间("$12.99 – $24.99"),绝不根据部分变体推算 |
endsAt | string? | 当前报价的截止时间:拍卖结束或限时优惠结束。ISO 8601 |
purchaseLimit | number? | 每位买家最多可购买的数量("每人限购 2 件") |
listedAt | string? | 商品首次在平台上架的时间(不是抓取时间)。ISO 8601 日期或时间 |
deliveryTo | object? | {label?, country?, postalCode?}——delivery 和 shippingOptions 的配送承诺所针对的收货地 |
ext | object? | 共享字段无法容纳、仅该平台才有的信息——见下 |
Money 是 {currency, amount},其中 amount 以十进制主单位表示。
ext 是最后的手段:只要有含义相同的共享字段,信息就放进该字段;只有其他平台都没有的信息,才以该平台自己的
名称放进 ext。商品、变体、卖家、报价、评价、评价页和类目节点都可能带有它。
DeliveryOption
所有配送信息都是这一种形状——delivery、shippingOptions 的每一项、报价的 delivery,以及
ShippingQuote 的每个选项。
| 字段 | 类型 | 说明 |
|---|---|---|
estimate | string? | 按原文表述的送达预估:"FREE delivery Wednesday, October 28" |
cost | Money? | 运费 |
free | boolean? | 平台声明包邮 |
minDays maxDays | number? | 以数字给出时的运输天数区间 |
handlingDays | number? | 卖家发货前所需的天数 |
guaranteedDays | number? | 平台自己的送达保证 |
cutoff | string? | 该选项的下单倒计时:"Order within 3 hrs 22 mins" |
condition | string? | 适用条件:"on orders shipped by Amazon over $35" |
available | boolean? | false——平台不向所请求的地区发货 |
membership | string? | 此选项所需的会员资格,例如 "Prime";缺省 ⇒ 适用于所有买家 |
minimumOrder | Money? | cost/free 生效所需的订单金额——如Amazon的 "on $35 of items" |
carrier code | string? | 平台所称的配送服务 |
tracking | boolean? | |
shipsFrom | string? | 包裹发出的国家 |
没有 cost 表示未说明,而不是包邮;包邮是 free: true。available: false 是一个答案,绝不等于运费为零。只要平台展示了,delivery 始终是适用于所有买家的选项——
会员专享的包邮(Prime)带着 membership 放在 shippingOptions 中,绝不会被当作所有人的条件。
Seller
| 字段 | 类型 | 说明 |
|---|---|---|
id name url | string? | |
rating | Rating? | 卖家的评分,与商品的 rating 形状相同 |
country | string? | 卖家所在地 |
detailedRatings | object? | {itemAsDescribed?, communication?, shippingSpeed?},采用平台自己的 0–5 分制 |
positivePercent | number? | 卖家反馈中好评所占的比例,以 0–100 的百分数表示("99.8% positive"),不是星级评分 |
rating.grouped: true 表示该汇总覆盖整个变体系列,而不是所请求的那一个变体。该字段缺失意味着不确定
是否被合并,这比"确定是按变体统计的"要弱。
仅 SearchItem
| 字段 | 类型 | 说明 |
|---|---|---|
imageUrl | string? | 卡片的主图 |
sponsored | boolean? | 付费展示位,而非自然结果 |
variantCount | number? | 卡片上标明的组合数量("+12 colors");product 会在 variants 中逐一列出 |
category | CategoryStep[]? | 该结果在类目体系中的位置,叶子类目在最后;卡片通常只带叶子类目 |
matchScore | number? | 仅限以图搜索:与所传图片的相似程度,取值 0–1(1 = 平台认定的最佳匹配),只能在同一组结果内比较 |
仅 Product
| 字段 | 类型 | 说明 |
|---|---|---|
description | string? | 可能是 HTML |
features | string[] | 页面顺序排列的要点 |
media | object | {images: string[], videos: []} |
category | CategoryStep[] | 完整路径,末级在最后;每一级有 id、name,或两者 |
options | Option[] | 各个维度:颜色、尺码 |
variants | Variant[] | 各种组合——见下 |
offers | Offer[] | 平台会逐条列出时的竞争卖家 |
subscriptionPrice | Money? | Subscribe & Save 及同类 |
reviewSummary | string? | 平台自己生成的评价摘要 |
reviewAspects | array | 带标签的情感分析:{name, mentions?, positive?, negative?} |
boxContents | string[] | 包装清单 |
brandUrl fulfilledBy replacedBy | string? | |
package | object? | 寄出的包裹:{lengthCm?, widthCm?, heightCm?, weightKg?, soldInLot?} |
wholesale | boolean? | 该商品提供批发价(见 variants[].bulkPrice) |
reviews | Review[] | 商品页自身携带的热门评价 |
included | string[]? | 所请求的扩展中实际取到的部分 |
变体,以及唯一需要检查的那件事
现实中存在两种变体模型,一个字段就能区分:
- 存在
variant.productId——该组合本身就是可单独获取的商品。把这个 id 回传给product。Amazon属于 这一类。 - 没有
variant.productId——该组合只存在于这个商品内部,你需要的东西都已经拿到了。Shopify、Walmart 与AliExpress属于这一类。
你永远不需要知道自己在跟哪个平台打交道,检查这个字段即可。
| 变体字段 | 类型 | 说明 |
|---|---|---|
id | string | 平台对该组合的 id |
productId | string? | 存在 ⇒ 可以用 product 获取(见上) |
sku | string? | 商家自己的库存编码 |
price listPrice | Money? | |
bulkPrice | Money? | 批量购买时的单价,仅在与 price 不同时给出 |
purchaseLimit | number? | 单个买家最多可购买的数量 |
taxIncluded | boolean? | price 是否含税 |
inStock | boolean? | |
stock | number? | 该变体的可售数量 |
identifiers | object? | {gtin?, mpn?, model?}——平台按变体公布时,该组合自己的条码和零件号 |
weightKg | number? | 该组合的重量(千克) |
selections | array | {optionId, valueId, optionName, valueName}——它对应哪些选项取值 |
Offer
平台会逐条列出时的竞争卖家:{seller?, price, listPrice?, condition?, conditionNote?, fulfilledBy?, inStock?, status?, delivery?}。delivery 是一个 DeliveryOption,因为更便宜但发货更晚的报价是另一个报价。
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
}| 字段 | 类型 | 说明 |
|---|---|---|
summary | Rating? | 描述 reviews 所取自的那个集合 |
page.total | number? | 平台给出的列表中的评价条数,可能少于统计全部评分的 summary.count(Walmart 只列出带文字的评价)。0 表示该商品没有评价 |
reviews[] | Review | {id?, author?, rating, title?, body?, date?, country?, verified?, helpfulCount?, variant?, images[], incentivized?, reply?} |
grouped | boolean? | 这些是整个系列的评价,而不只是这个商品的 |
cursor | string? |
没有评价的商品
它会以明确的零返回 200,因此你可以放心地显示"0 条评价":
{ "marketplace": "shopify", "productId": "shop.com/handle", "page": { "index": 1, "size": 0, "total": 0 }, "reviews": [] }page.total: 0 只在商品确实没有任何评价时才会发出。如果有证据表明该商品确实有评价但取不到——报出了
条数或平均分,或者商品页上能看到评价——你会收到一个错误,而绝不是零。因此一个空的 reviews 数组只可能
意味着两件事之一,而 page.total 会告诉你是哪一件:
page.total | 含义 |
|---|---|
0 | 该商品没有评价 |
| 其他任何值(数字,或缺失) | 你已经翻过了评价的末尾 |
返回零时不会有 summary:没有平均分可报。
grouped 是必须披露的事实,而不是细节。商家可以用整个系列的 9,000 条评价来回答某一种颜色的页面,而评价
本身不会有任何提示——在被合并的页面上计算"这个商品的评分",得到的是整个系列的评分。
review.incentivized: true 表示平台将这条评价标注为获得回报(免费商品、样品、奖励)后撰写;没有该字段只表示
没有标注,绝不代表"确认未获回报"。review.reply 是商家的公开回复,形如 {body, date?, author?}。
当平台给出时,review.variant 会指明该评价是针对哪一个同系列商品写的。在被合并的页面里,variant 为空表示
平台没有说明,绝不表示"这条是针对你所请求的那个商品的"。
page.total 与 summary.count 统计的对象不同:一个商品可以报出 7,654 个评分,而它的评价列表只收录 990 条带文字的评价。
count 忠实于商品,total 忠实于列表。两者都不保证每条评价都能取到:各平台对可翻页的深度设有上限,cursor 结束时列表即结束。
CategoryList
categories 返回的内容:一份扁平列表,每个节点都指明自己的父级。之所以扁平而非嵌套,是因为常见的请求是
"整棵分类树",而嵌套会让它在传输上更大、也更难分页。重建树结构只是按 parentId 做一次 groupBy。
{
"marketplace": "aliexpress",
"path": [ { "id": "200000345", "name": "Women's Clothing" } ],
"categories": [
{ "id": "349", "name": "Skirts", "parentId": "200000345" },
{ "id": "201531101", "name": "Matching Sets", "parentId": "200000345" }
]
}| 字段 | 类型 | 说明 |
|---|---|---|
marketplace | string | |
path | CategoryStep[] | 所请求类目的各级祖先,末级在最后。位于根时为空。 |
categories | CategoryNode[] |
CategoryNode
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 你要回传的 id——下钻时传给 categories,browsable 允许时传给 browse |
name | string | |
parentId | string? | 位于根时没有 |
hasChildren | boolean? | 缺失表示未知,而不是"没有"——多数平台并不说明 |
browsable | boolean? | 见下 |
productCount | number? | 仅当平台给出时;绝不从页面上自行统计 |
与本结构的大多数字段不同,id 与 name 都是必填。类目列表存在的意义就是被使用:没有 id 的条目无法
使用,没有名称的条目无法阅读。
browsable——最值得仔细读的一个字段
缺失表示可以,这也是常态。false 标记的是真实存在、可以导航,但背后没有商品列表页的层级。
这里的每个平台都有这样的层级,而且并非个别情况:
| 不可列出商品的是 | |
|---|---|
| Amazon | 各个部门(它们是店铺 slug,不是 browse 节点) |
| Walmart | 部分部门——Electronics 会列出商品,Toys 则是没有商品网格的枢纽 |
遇到这种情况时,用 categories 再下钻一层,那里的 id 就可以列出商品了。
给 categories 传 browsableOnly=true,只会得到 browse 接受的 id——费用与何时返回 400,见
平台概览。
你不必信任这个标志
browsable 只在已经确认的情况下才会被填上,因此一个无法列出商品的层级仍可能以缺失该字段的形式——也就是
乐观地——返回给你。在Walmart上这无法避免:Electronics(3944)与 Toys(4171)都是有子类目的部门,其中
一个会列出商品,另一个不会,而分类数据里没有任何东西能区分它们。
所以 browse 会直接告诉你。对一个没有商品列表页的类目发起请求,返回的是
{ "success": false, "error": { "code": "CATEGORY_NOT_BROWSABLE", "message": "Walmart category 4171 is a navigation hub…" } }而不是一个空列表——因为售罄的类目返回的同样是空列表,你将无从分辨"请下钻到子类目"与"这里没有在售商品"。
请把这个代码当作一条指令:用同一个 id 调用 categories,再浏览它的某个子类目。它是 400,而不是请求失败:
重试不会改变答案。
ShippingQuote
shipping 返回的内容:一件商品发往一个地区的全部配送方式,最便宜的在前。
{
"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 是 DeliveryOption。available: false 且没有选项,表示平台不向该地区发货——
这是一个答案,而不是错误。
错误
{ "success": false, "error": { "code": "INVALID_PARAMS", "message": "…" } }| 状态 | 代码 | 含义 |
|---|---|---|
| 400 | INVALID_PARAMS | 参数缺失、格式错误,或不是该平台接受的参数 |
| 400 | CATEGORY_NOT_BROWSABLE | 类目存在但没有商品列表页——用 categories 下钻并浏览某个子类目 |
| 404 | NOT_FOUND | 该 id 不存在 |
| 404 | UNKNOWN_MARKETPLACE / UNKNOWN_OPERATION | 请查看 GET /v1/marketplaces |
| 429 | 触发限流 | 附带 Retry-After |
| 501 | UNSUPPORTED | 数据存在,但本 API 无法提供——不是你的请求有问题 |
| 502 | 上游故障 | 平台不可用;请重试 |
501 有意不是 404:"这些评价无法提供"绝不能被读成"这个商品没有评价"。