《亚马逊 SP-API 商品API:getCatalogItem 与 searchCatalogItems 的 ASIN / UPC / EAN / GTIN 映射踩坑》(附 Python 源码)
一、先拍结论
亚马逊商品域最阴的不是“调不通”,而是标识符语义不对等:
ASIN = 亚马逊目录内部键(10 位,按 marketplace 可能不同)
UPC / EAN / JAN / ISBN = 外部 GTIN 体系
GTIN = UPC/EAN/JAN/ISBN 的父概念
SKU = 卖家自己的库存键,不是亚马逊目录键
官方原话:“Amazon catalog makes no guarantees about a 1:1 mapping between external product identifiers and ASINs. An ASIN can have multiple external product identifiers, and an external product identifier can map to multiple ASINs.”所以:
getCatalogItem:ASIN → 目录真相(路径参数)
searchCatalogItems:UPC/EAN/GTIN/关键词 → 可能多个 ASIN不传
includedData=identifiers:UPC/EAN 根本不返回跨站:US 的 ASIN ≠ DE 的 ASIN,但 UPC 可能同一条
变体:父 ASIN 不可买,子 ASIN 才有库存/价格/条码
二、两个接口的职责边界
接口 | 入参 | 返回 | 生产角色 |
|---|---|---|---|
GET /catalog/2022-04-01/items/{asin} | ASIN(路径)+ marketplaceIds | 单品属性/图/标识/变体 | 已知 ASIN 拉真相 |
GET /catalog/2022-04-01/items?identifiers=...&identifiersType=UPC | UPC/EAN/GTIN/ASIN/ISBN/JAN/MINSAN/SKU | 可能多个 ASIN | 条码反查 ASIN |
keywords 搜索 | 关键词 | 估算总量,最大翻 1000 个 | 选品/建品前匹配 |
getCatalogItem不接受 UPCsearchCatalogItems里identifiers和keywords不能同时传identifiersType=SKU必须带sellerId默认
includedData=summaries,UPC/EAN 不出来
三、标识符映射矩阵(必须记死)
外部码 | 位数 | 区域 | 能否反查 ASIN | 备注 |
|---|---|---|---|---|
UPC | 12 | 北美 | 能,但可能多 ASIN | GS1 颁发,伪造会 5665 |
EAN | 13 | 欧洲/全球 | 能 | 和 UPC 不保证一一对应 |
JAN | 8/13 | 日本 | 能 | EAN 子集 |
ISBN | 10/13 | 图书 | 能 | 图书 ASIN 常直接用 ISBN |
GTIN | 8/12/13/14 | 全球 | 能 | UPC/EAN/JAN 的统称 |
ASIN | 10 | 亚马逊 | 本身就是目标 | 不同 marketplace 可能不同 |
SKU | 自定义 | 卖家 | 不直接查目录 | 要走 Listings API |
UPC ──┐ EAN ├──> GTIN 体系 ──> 亚马逊匹配/新建 ASIN ──> 1 个 UPC 可能对应多个 ASIN JAN ┘ ↑ ASIN <───────────────────────────────────┘ 一个 ASIN 也可能挂多个 GTIN
四、踩坑清单(跨境 ERP 最常用错)
坑 1:以为 UPC → ASIN 是 1:1
同 UPC 在 US / CA 可能生成不同 ASIN
一个 ASIN 页面可能列 UPC + EAN + JAN
图书/捆绑包/变体父商品更乱
searchCatalogItems(UPC) → 得到 N 个 ASIN → 每个 ASIN 再 getCatalogItem(includedData=attributes,identifiers,relationships) → 用 brand / product_type / title / variation_theme / parent_child 判定
坑 2:没传 includedData=identifiers,然后说“接口不返回 UPC”
summaries。"identifiers": [
{"marketplaceId": "ATVPDKIKX0DER", "identifiers": [
{"identifierType": "UPC", "identifier": "012345678901"},
{"identifierType": "EAN", "identifier": "4006381333931"}
]}
]坑 3:把父 ASIN 当可售 SKU
Parent ASIN (VARIATION_PARENT, 不可买) ├─ Child ASIN A (Red / M) ├─ Child ASIN B (Red / L) └─ Child ASIN C (Blue / M)
坑 4:跨站直接复用 ASIN
US: B0XXXXXX01 DE: B0YYYYYY02
f"{marketplace_id}:{asin}"asin 本身。坑 5:用伪造/非 GS1 UPC 建品
5665GTIN validation failed5461invalid product id8572identity mismatch(UPC 属品牌 A,你填品牌 B)
坑 6:searchCatalogItems 限流当普通接口刷
Rate: 2 rps Burst: 2
pageSize ≤ 20、identifiers ≤ 20、最多翻 1000 个结果。每批 ≤ 20 个
令牌桶 2 rps
429 读
Retry-After结果缓存 2~24h(商品基础信息比价格稳)
五、Python:Catalog Client(SigV4 + LWA 已略,复用上篇)
# spapi/catalog_client.py
import time
import requests
class SpCatalogClient:
def __init__(self, token_provider, region_endpoint,
rate_per_sec=2.0, burst=2):
self.tp = token_provider
self.base = region_endpoint # na/eu/fe
self.rate = rate_per_sec
self.burst = burst
self._tokens = burst
self._last = time.monotonic()
def _acquire(self):
while True:
now = time.monotonic()
self._tokens = min(self.burst, self._tokens + (now - self._last) * self.rate)
self._last = now
if self._tokens >= 1:
self._tokens -= 1
return
time.sleep(0.1)
def _get(self, path, params):
for attempt in range(5):
self._acquire()
token = self.tp.get_access_token()
headers = {"x-amz-access-token": token, "Accept": "application/json"}
r = requests.get(self.base + path, headers=headers, params=params, timeout=10)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 30)) + attempt * 2)
continue
if r.status_code >= 500:
time.sleep(2 ** attempt)
continue
r.raise_for_status()
return r.json()
raise RuntimeError("catalog request failed")
def get_by_asin(self, asin, marketplace_ids, included_data=None):
params = {"marketplaceIds": ",".join(marketplace_ids)}
if included_data:
params["includedData"] = ",".join(included_data)
return self._get(f"/catalog/2022-04-01/items/{asin}", params)
def search_by_identifier(self, identifiers, id_type, marketplace_ids, included_data=None):
params = {
"identifiers": ",".join(identifiers),
"identifiersType": id_type, # UPC / EAN / GTIN / ASIN / ISBN / JAN / MINSAN / SKU
"marketplaceIds": ",".join(marketplace_ids),
}
if included_data:
params["includedData"] = ",".join(included_data)
return self._get("/catalog/2022-04-01/items", params)六、Python:UPC → 候选 ASIN → 真相校验
# spapi/upc_to_asin.py
from dataclasses import dataclass
from typing import List
@dataclass
class CandidateAsin:
asin: str
brand: str
product_type: str
title: str
parent_child: str
gtins: list
class UpcResolver:
def __init__(self, catalog: "SpCatalogClient", marketplace_ids):
self.cat = catalog
self.mp = marketplace_ids
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
def resolve(self, upc: str) -> List[CandidateAsin]:
resp = self.cat.search_by_identifier(
[upc], "UPC", self.mp,
included_data=["summaries", "attributes", "identifiers", "relationships"],
)
items = resp.get("items", [])
out = []
for it in items:
asin = it.get("asin")
detail = self.cat.get_by_asin(
asin, self.mp,
included_data=["summaries", "attributes", "identifiers", "relationships"],
)
out.append(self._parse(asin, detail))
return out
def _parse(self, asin, detail):
summaries = detail.get("summaries", [{}])[0]
attrs = detail.get("attributes", {})
# 亚马逊 attributes 是 { "brand": [{"value":"X"}], "item_name": [{"value":"Y"}] }
def first(key):
v = attrs.get(key)
return v[0]["value"] if isinstance(v, list) and v else None
gtins = []
for blk in detail.get("identifiers", []):
for idn in blk.get("identifiers", []):
gtins.append((idn.get("identifierType"), idn.get("identifier")))
rel = (detail.get("relationships") or [{}])[0] if detail.get("relationships") else {}
return CandidateAsin(
asin=asin,
brand=first("brand"),
product_type=summaries.get("productType"),
title=first("item_name") or summaries.get("itemName"),
parent_child=rel.get("type") if rel else None,
gtins=gtins,
)
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
def best_match(self, candidates: List[CandidateAsin], brand=None, product_type=None):
scored = []
for c in candidates:
s = 0
if c.parent_child == "CHILD":
s += 10
if brand and c.brand and c.brand.lower() == brand.lower():
s += 5
if product_type and c.product_type == product_type:
s += 3
scored.append((s, c))
scored.sort(key=lambda x: x[0], reverse=True)
return scored[0][1] if scored else Noneresolver = UpcResolver(catalog, ["ATVPDKIKX0DER"])
cands = resolver.resolve("012345678901")
target = resolver.best_match(cands, brand="Acme", product_type="SHIRT")
print(target.asin, target.gtins)七、统一商品主数据模型(接前几篇)
{
"platform": "amazon",
"marketplace_id": "ATVPDKIKX0DER",
"spu_key": "amazon:ATVPDKIKX0DER:B0XXXXXX01", # 父/子都算 item
"asin": "B0XXXXXX01",
"seller_sku": "ACME-TSHIRT-RED-M", # 来自 Listings API,不来自 Catalog
"gtins": {
"UPC": "012345678901",
"EAN": "4006381333931"
},
"attrs": {"color": "Red", "size": "M", "brand": "Acme"},
"item_classification": "PRODUCT / VARIATION_PARENT / PRODUCT_BUNDLE",
"parent_asin": "B0PARENT01",
"images": {"MAIN": "https://m.media-amazon.com/...", "PT01": "..."},
"fingerprint": sha1(json(sorted(attrs)))
}淘宝/京东/拼多多/抖店:
platform + spu_id + sku_id亚马逊:
marketplace_id + asin才是目录主键UPC/EAN 只能做“外部匹配键”,不能做主键
seller_sku 只在你自己的店铺里有意义,跨卖家无意义
八、建品顺序(UPC → ASIN → Offer)
1. 有 GS1 GTIN → searchCatalogItems(UPC/EAN) 2. 命中 ASIN → getCatalogItem 校验 brand/product_type/variation 3. 没命中 → 用 Product Type Definitions 建 catalog item 4. 等目录记录传播(可能几小时~48h) 5. 再发 Listings / Feeds 挂 offer(seller_sku + price + fulfillment)
目录还没建好就发 offer →product not in catalog/can't find matching ASIN。
九、图片坑(和抖店不同)
亚马逊图链在
m.media-amazon.com,不是永久对象存储铺到独立站/1688/抖店要下载转存 OSS/S3
版权归品牌/摄影师,转存 ≠ 可商用
images里variant=MAIN/PT01/SWCH,SWCH 是色卡缩略图,不是主图
十、限流与缓存策略
数据 | 缓存时长 | 原因 |
|---|---|---|
属性/类目/品牌 | 2~24h | 很少变 |
图片 URL | 1~6h | 可能失效 |
销售排名 | 15~30min | 会变 |
价格/库存 | 走 Listings/Offers/Reports | 别用 Catalog 看实时库存 |
UPC→ASIN 映射 | 7~30 天 | 目录匹配稳定,但新上架要失效 |
库存:Inventory API / FBA Reports
价格:Offers / Pricing API
目录:Catalog Items API
十一、落地检查表
[ ]
getCatalogItem只用 ASIN,不传 UPC[ ]
searchCatalogItems必须带identifiersType[ ] 想要 UPC/EAN 必须
includedData=identifiers[ ] UPC→ASIN 结果做多候选打分,不取第一个
[ ] 父 ASIN 不进可售 SKU 表
[ ] 主键用
marketplace_id + asin[ ] 跨站不共享 ASIN
[ ] 建品先目录后 offer,遇到 not in catalog 等传播
[ ] GTIN 用 GS1 注册,假 UPC 会 5665/8572
[ ] Catalog 不限流当搜索框刷,2 rps 也要桶
[ ] 图片转存自有 CDN,但别侵犯版权
十二、一句话收口
淘宝用num_iid、京东用skuId、拼多多用goods_id+sku_id、抖店用product_id+sku_id,亚马逊最拧巴:ASIN 是亚马逊的,UPC/EAN 是 GS1 的,SKU 是你的,GTIN 是抽象父类。谁把 UPC 当主键、把父 ASIN 当可售品、把 summaries 当完整数据、把 Catalog 当库存接口、把 offer 建得比 catalog 还快——谁就会在亚马逊商品同步里反复看到:多 ASIN、空 identifiers、not in catalog、变体串色、跨站丢图、GTIN 被拒。