×

《亚马逊 SP-API 商品API:getCatalogItem 与 searchCatalogItems 的 ASIN / UPC / EAN / GTIN 映射踩坑》(附 Python 源码)

万邦科技Lex 万邦科技Lex 发表于2026-10-19 09:39:49 浏览30 评论0

抢沙发发表评论

《亚马逊 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 不接受 UPC

  • searchCatalogItems 里 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。
UPC/EAN 在:
"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)
ERP 里要卖/要库存/要条码的是子 ASIN。
父 ASIN 只持有标题、品牌、变体主题。

坑 4:跨站直接复用 ASIN

US: B0XXXXXX01
DE: B0YYYYYY02
UPC 相同,但 ASIN 不同。
跨平台中台里主键必须是:
f"{marketplace_id}:{asin}"
不是 asin 本身。

坑 5:用伪造/非 GS1 UPC 建品

报错:
  • 5665 GTIN validation failed

  • 5461 invalid product id

  • 8572 identity mismatch(UPC 属品牌 A,你填品牌 B)

✅ 真实货品用 GS1 注册 GTIN;自有手工/定制/捆绑可申请 GTIN Exemption。

坑 6:searchCatalogItems 限流当普通接口刷

默认:
Rate: 2 rps
Burst: 2
还支持 pageSize ≤ 20、identifiers ≤ 20、最多翻 1000 个结果。
✅ 批量 UPC 反查要:
  • 每批 ≤ 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 None
调用:
resolver = 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 天
目录匹配稳定,但新上架要失效
Catalog API 不是库存接口:
  • 库存: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 被拒。


群贤毕至

访客