《抖音商品API:/product/detail 与 /product/list 的字段边界与图片合规踩坑》(附 Python 源码)
一、先拍结论
抖店商品域最容易出事的地方不是“调不通”,而是:把/product/list当详情用、把pic当 SKU 图用、把白底图当首图用、把平台返回图链当永久 CDN 用、把类目属性当可选填。
/product/list:列表视图,适合兜底/增量/比状态,字段是“缩略版”
/product/detail:商品真相源,返回规格、SKU、图片、类目、属性、审核态
pic:商品轮播主图
spec_pics:SKU 规格图,要用spec_detail_id关联,不是用spec_id硬取图片合规:首图实物无字、白底图单独传、3:4 或 1:1、无二维码/无极限词/无第三方水印
类目属性:
product_format_new等必填属性不填 → 发品直接被审核打回
二、两个接口的边界(别再混)
维度 | /product/list / /product/listV2 | /product/detail |
|---|---|---|
定位 | 商品列表/分页/状态扫描 | 单品真相 |
入参 | page/page_size/status/update_time | product_id 或 out_product_id |
返回 | 基础字段:标题、状态、价格、主图、类目 | 完整:spec、sku、spec_pics、desc、类目属性 |
成本 | 列表类调用,店群别乱翻页 | 详情类调用,按商品触发 |
用法 | 兜底漏单、状态机推进 | 写商品主数据、SKU 映射、图片合规校验 |
坑 | 以为列表里有完整 SKU 图 | 以为 detail 不收费/可以无限拉 |
list = 找哪些商品变了 detail = 把变了的那个商品看清楚 sku/list = 拿 SKU 级 spec_detail_id spec_pics = 按 spec_detail_id 关联 SKU 图
product/detail 返回 spec_pics,但 不能用 /sku/list 的 spec_id 去取 spec_pics,要用 spec_detail_id1 和 spec_pics.spec_detail_id 关联;图片链接往往还要拼固定域名。三、核心字段边界
1. 商品级
{
"product_id": "3558192687276554544",
"out_product_id": "dy001", // 新推荐;out_product_id 是老字段,逐渐废弃
"name": "纯棉短袖T恤 男 夏季",
"pic": ["https://p3-.../a.jpg"], // 商品轮播主图,不是 SKU 图
"status": 1, // 上下架
"check_status": 2, // 审核状态
"cid": 12345, // 类目
"product_format_new": {...}, // 类目必填属性
"description": "<p>详情页HTML</p>"
}2. SKU 级
{
"sku_id": "123",
"out_sku_id": 456,
"outer_sku_id": "A001", // 新推荐 string 版
"spec_detail_id1": "1001",
"spec_detail_id2": "1002",
"spec_detail_name1": "颜色",
"spec_detail_name2": "尺码",
"price": 3900, // 分
"stock_num": 10
}3. 规格图级
{
"spec_pics": [
{"spec_detail_id": "1001", "url": "https://p3-.../red.png"},
{"spec_detail_id": "1002", "url": "https://p3-.../blue.png"}
]
}sku_spec_detail_id = sku["spec_detail_id1"] sku_img = next(p["url"] for p in spec_pics if p["spec_detail_id"] == sku_spec_detail_id)
sku_img = spec_pics[sku["spec_id"]] # 错!spec_id ≠ spec_detail_id
四、图片合规:抖店不是“能传上去就合规”
1. 主图规则(官方口径)
第一张主图:商品主体正面实物图
所有主图:除品牌 logo 外,不得有文字/水印
辅图:侧面/背面/平铺/细节,不能全一个角度
主图商品数量/颜色/规格要和 SKU 一致
不得有二维码、微信号、第三方平台水印、非平台链接
2. 比例与尺寸
1:1:
800×800,建议1200×12003:4:
750×1000,信息流更吃这个白底图:纯白、无阴影、无 logo、主体占 70%+
详情页:宽
750,总高别超10000主图视频:9:16,1080×1920,MP4
3. 白底图误区
白底图不是“首图必须白底”,而是“商城流量/活动报名的加分素材”。
五、类目与必填属性:发品被打回的重灾区
/product/detail 里你能看到类目和属性,但发品/编辑时:cid选错 → 类目错放(严重违规,下架+扣分)product_format_new必填属性没填 → 审核不通过必填属性要通过
/product/getCatePropertyV2拿,不是拍脑袋编辑商品时:
spec_prices是大 JSON,不传的字段不更新,传就要传完整结构
pdd_cat_path = "男装 > T恤 > 短袖T恤" tb_cid3 = 50012029 jd_cid3 = 998 douyin_cid = 12345 # 跨平台别用 cat_id 直连,用“类目路径 + 属性词”做映射
六、Python:detail / list 拉取 + 字段归一
# douyin/product_client.py
import time
import hashlib
import requests
import hmac
class DouyinProductClient:
GATEWAY = "https://openapi-fxg.jinritemai.com/"
def __init__(self, app_key, app_secret, access_token, sign_method="hmac-sha256"):
self.app_key = app_key
self.app_secret = app_secret
self.access_token = access_token
self.sign_method = sign_method
def _sign(self, params: dict) -> str:
qs = "&".join(f"{k}={params[k]}" for k in sorted(params))
if self.sign_method == "hmac-sha256":
return hmac.new(
self.app_secret.encode(), qs.encode(), hashlib.sha256
).hexdigest()
return hashlib.md5(
(self.app_secret + qs + self.app_secret).encode()
).hexdigest().upper()
def _common(self, method: str):
return {
"method": method,
"app_key": self.app_key,
"access_token": self.access_token,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
"v": "2",
"sign_method": self.sign_method,
"format": "json",
}
def list_products(self, status=None, page=1, page_size=100, start=None, end=None):
biz = {"page": page, "page_size": page_size, "order_by": 1}
if status is not None:
biz["status"] = status
if start:
biz["start_time"] = start
if end:
biz["end_time"] = end
params = self._common("product.list")
params["param_json"] = str(biz).replace("'", '"')
params["sign"] = self._sign(params)
return requests.post(self.GATEWAY, data=params, timeout=5).json()
def get_detail(self, product_id=None, out_product_id=None, show_draft=False):
biz = {"show_draft": show_draft}
if product_id:
biz["product_id"] = product_id
elif out_product_id:
biz["out_product_id"] = out_product_id
else:
raise ValueError("product_id/out_product_id 至少传一个")
params = self._common("product.detail")
params["param_json"] = str(biz).replace("'", '"')
params["sign"] = self._sign(params)
return requests.post(self.GATEWAY, data=params, timeout=5).json()抖店param_json是字符串化 JSON,不是散参数;timestamp是yyyy-MM-dd HH:mm:ss(GMT+8)。
七、Python:SKU 图按 spec_detail_id 关联
# douyin/spec_pic_resolver.py
from dataclasses import dataclass
from typing import Dict, List, Optional
@dataclass
class ResolvedSku:
sku_id: str
outer_sku_id: Optional[str]
attrs: Dict[str, str]
price_yuan: float
stock: int
sku_img: Optional[str]
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
def resolve_skus(detail: dict) -> List[ResolvedSku]:
d = (detail.get("data") or {})
spec_pics = {p["spec_detail_id"]: p["url"] for p in d.get("spec_pics", [])}
out = []
for sku in d.get("skus", d.get("spec_prices", [])):
attrs = {}
for i in (1, 2, 3):
name = sku.get(f"spec_detail_name{i}")
val = sku.get(f"spec_detail_value{i}") or sku.get(f"spec{i}_name")
if name:
attrs[name] = val
# 一级规格图最常见;多级可拼 [sd1, sd2]
sd = sku.get("spec_detail_id1")
sku_img = spec_pics.get(sd) if sd else None
price_fen = sku.get("price") or sku.get("sku_price") or 0
stock = sku.get("stock_num") or sku.get("available_stock") or 0
out.append(ResolvedSku(
sku_id=str(sku["sku_id"]),
outer_sku_id=sku.get("outer_sku_id") or str(sku.get("out_sku_id", "")),
attrs=attrs,
price_yuan=price_fen / 100.0,
stock=int(stock),
sku_img=sku_img,
))
return out八、Python:图片合规预检(上架前拦废图)
# douyin/image_compliance.py
import re
from urllib.parse import urlparse
FORBIDDEN_TEXT_HINTS = ["微信", "加我", "扫码", "薇信", "私聊", "淘宝", "京东", "拼多多"]
EXT_OK = {".jpg", ".jpeg", ".png"}
LIMIT_MB = 3
class ImageComplianceError(Exception):
pass
def check_main_images(pics: list, first_must_be_real=True):
if not pics or len(pics) < 3:
raise ImageComplianceError("主图至少 3 张")
if len(pics) > 9:
raise ImageComplianceError("主图最多 9 张")
for url in pics:
p = urlparse(url)
ext = "." + p.path.rsplit(".", 1)[-1].lower()
if ext not in EXT_OK:
raise ImageComplianceError(f"格式不支持: {url}")
if any(h in url.lower() for h in ("qrcode", "wechat", "vip")):
raise ImageComplianceError(f"疑似二维码/站外水印: {url}")
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
def check_title(title: str):
if not title:
raise ImageComplianceError("标题为空")
if len(title) > 30:
raise ImageComplianceError("标题超过 30 个汉字")
bad = ["第一", "最高级", "全网最低", "百分百", "根治", "包治", "最便宜"]
hit = [w for w in bad if w in title]
if hit:
raise ImageComplianceError(f"标题含极限/违规词: {hit}")
def check_attr_completeness(product_format_new: dict, required_keys: list):
missing = [k for k in required_keys if not product_format_new.get(k)]
if missing:
raise ImageComplianceError(f"类目必填属性缺失: {missing}")check_title(name) check_main_images(pic) check_attr_completeness(product_format_new, required_keys) # 过了再进 addV2 / editV2
九、list → detail → sku 的生产流水线
/product/listV2 └─ 拿到 product_id + status + update_time └─ 状态/时间变化 → 进待同步队列 Worker: /product/detail(product_id) ├─ 归一商品主数据(标题/类目/主图/详情) ├─ resolve_skus() → SKU 表 ├─ spec_pics 按 spec_detail_id 关联 ├─ check_title / check_main_images / check_attr_completeness └─ 写 erp_spu / erp_sku / platform_product_snapshot 编辑回写: /product/editV2 - spec_prices 要传完整结构 - 不传的字段不会被更新
十、编辑商品的两个暗雷
1. spec_prices 是整体结构
{"spec_prices": [{"sku_id": 1, "price": 3990}]}spec_prices,改其中字段再回写。2. 审核中和在售商品
编辑在售商品:审核通过后才会覆盖线上数据
审核中商品:别并发编辑,别用 list 里的缩略数据回写 detail 级字段
十一、跨平台 SKU 归一(接前几篇)
{
"platform": "douyin",
"spu_id": product_id,
"sku_id": sku_id,
"outer_sku_id": outer_sku_id,
"attrs": {"颜色": "红", "尺码": "M"},
"sku_fingerprint": sha1(json.dumps(sorted(attrs))),
"price": 39.9,
"stock": 10,
"images": {
"product_pics": [...], # 商品主图
"sku_spec_pic": "https://..." # 规格图,按 spec_detail_id 关联
}
}pic≠spec_picsspec_id≠spec_detail_idout_sku_id(int) ≠outer_sku_id(string)跨平台别用平台 cat_id 直连,用类目路径+属性词
十二、落地检查表
[ ] list 只做“哪些商品变了”,不写主数据
[ ] detail 才写 SPU/SKU/图片/属性
[ ] SKU 图用
spec_detail_id关联spec_pics[ ] 主图 ≥3 张、≤9 张,首图实物无字
[ ] 白底图单独传,不强制当首图
[ ] 3:4 / 1:1 都支持,服装鞋包优先 3:4
[ ] 标题 ≤30 汉字,无极限词/站外词
[ ] 详情页无二维码/微信号/第三方水印
[ ]
product_format_new必填属性用getCatePropertyV2拿[ ] editV2 的
spec_prices传完整结构[ ] 图片 URL 拼域名后再存,别直接信相对路径
[ ] 列表调用和详情调用分桶计费(2026.7.1 起都值钱)
十三、一句话收口
淘宝商品难在properties_name,京东难在skuList嵌套,拼多多难在specs + sku_list.specs自己笛卡尔,抖店商品难在:list 和 detail 的字段边界、spec_id 和 spec_detail_id 的图关联、首图和白底图的合规语义、类目必填属性的审核拦截。谁把/product/list的pic当成 SKU 图、把白底图塞成首图、把spec_id拿去查spec_pics、把必填属性当可选填——谁就会看到:商品能发上去,但流量起不来、审核被打回、SKU 图串色、跨平台主数据全歪。