Files

16 KiB
Raw Permalink Blame History

name, description, version, tags
name description version tags
ozon-seller-api Ozon Seller API 操作手册 — 产品导入、审核、价格、分类、属性。含v3/import实测避坑(分类用desc_cat_id+type_id、属性用id不用attribute_id、字典属性需dict_id、价格必须CNY、审核检查图片匹配分类)。 0.2
ozon
seller-api
api
listings
cross-border-ecommerce

Ozon Seller API 操作手册

认证

Ozon Seller API 使用两个 Header 认证:

  • Client-Id: 店铺的 client_id(数字字符串)
  • Api-Key: 店铺的 API KeyUUID 格式,36字符)
  • Content-Type: application/json

无需 Bearer Token,不需要 JWT,直接 Header 传参即可。

店铺凭据(admincao 下)

Store client_id 说明
Ozon-Hzqjone 3098640 手机 15718853624
Ozon-Hzqjtwo 3103005 手机 13011158945

API Key 存储在 AtomK 后台 store 的 extra_data.api_key 字段,或本地 /home/ubuntu/.hermes/credentials/accounts.md

API 端点(已验证可用 vs 废弃)

可用端点

端点 用途 备注
POST /v3/product/list 获取产品ID列表 ⚠️ last_id 必须是空字符串 ""
POST /v3/product/info/list 批量获取产品详情 ⚠️ 响应结构陷阱见下方

废弃/404 端点

端点 状态 说明
POST /v2/product/list 404 已废弃
POST /v2/product/info 404 已废弃
POST /v2/product/info/list 404 已废弃
POST /v1/product/info 404 不存在
POST /v4/product/info 404 不存在
POST /v5/product/info 404 不存在
POST /v3/product/info 404 单个产品查询不存在

Base URL: https://api-seller.ozon.ru

核心陷阱与避坑

陷阱1v3/product/list 的 last_id 参数

# ❌ 错误 — 整数 0
{"last_id": 0}
# 报错:invalid value for string field last_id: 0

# ❌ 错误 — 字符串 "0"
{"last_id": "0"}
# 报错:invalid base64 data at input byte 0

# ✅ 正确 — 空字符串(首页)
{"last_id": ""}

last_id 是 base64 编码的分页游标,首页必须传空字符串 ""。后续页使用上一次返回的 last_id 值。

陷阱2v3/product/info/list 响应结构

# ❌ 错误 — 以为在 result.items 下
products = result.get("result", {}).get("items", [])
# 返回空列表,因为 result 键不存在

# ✅ 正确 — items 在顶层
products = result.get("items", [])

v3/product/list 返回 {"result": {"items": [...]}} 不同,v3/product/info/list 返回的是 {"items": [...]},没有 result 包裹层!

陷阱3v3/product/list 返回结构

{
  "result": {
    "items": [
      {
        "product_id": 2184055159,
        "offer_id": "XRZBQ_HK",
        "has_fbo_stocks": false,
        "has_fbs_stocks": true,
        "archived": false,
        "is_discounted": false,
        "quants": []
      }
    ],
    "total": 145,
    "last_id": "WzM5OTI4Mjk4MTAsMzk5MjgyOTgxMF0="
  }
}

注意:这个接口的响应是在 result 下的。

完整代码模板:获取全部 Active Listing

import urllib.request, json, ssl

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

CLIENT_ID = "3098640"
API_KEY = "your-api-key-here"

HEADERS = {
    "Content-Type": "application/json",
    "Client-Id": CLIENT_ID,
    "Api-Key": API_KEY
}

def ozon_request(endpoint, body_dict):
    url = f"https://api-seller.ozon.ru{endpoint}"
    data = json.dumps(body_dict).encode()
    req = urllib.request.Request(url, data=data, headers=HEADERS)
    resp = urllib.request.urlopen(req, timeout=20, context=ctx)
    return json.loads(resp.read())

# Step 1: 获取全部产品ID(分页)
all_product_ids = []
last_id = ""
while True:
    result = ozon_request("/v3/product/list", {
        "filter": {"visibility": "ALL"},
        "limit": 100,
        "last_id": last_id
    })
    items = result["result"]["items"]
    all_product_ids.extend([i["product_id"] for i in items])
    last_id = result["result"].get("last_id", "")
    if not last_id or len(items) < 100:
        break

# Step 2: 批量获取产品详情(每批50)
all_products = []
for offset in range(0, len(all_product_ids), 50):
    batch = all_product_ids[offset:offset+50]
    detail = ozon_request("/v3/product/info/list", {
        "product_id": batch
    })
    # ⚠️ 注意:items 在顶层,不在 result 下!
    all_products.extend(detail.get("items", []))

# Step 3: 筛选 Active
active = [p for p in all_products if not p.get("is_archived", False)]

产品详情关键字段

字段 说明 示例
id 产品ID,用于构建前台URL 2184055159
name 产品名称(俄语) "закладки"
offer_id 卖家自定义SKU "XRZBQ_HK"
price 当前售价 "7.28"
old_price 划线价 "15.61"
currency_code 货币 "CNY"
is_archived 是否归档 false
sources[].sku Ozon SKU 2449791222
sources[].source 发货方式 "sds"
sources[].shipment_type 配送类型 "SHIPMENT_TYPE_GENERAL"
stocks.has_stock 是否有库存 true
stocks.stocks[].source 库存来源 "fbs" / "fbo"
commissions 佣金信息 含 FBO/FBS/RFBS/FBP

Ozon 前台 URL

https://www.ozon.ru/product/{product_id}

注意:从中国直接访问 ozon.ru 会被地域封锁(307重定向),需要俄罗斯代理或VPN。

filter.visibility 选项

说明
ALL 全部产品
VISIBLE 仅可见(有库存+有价格)
IN_SALE 在售
NOT_SALE 未在售

依赖

无额外依赖,使用 Python 标准库 urllib + json + ssl 即可。不需要 requests 或其他第三方库。

产品导入 (Product Import) — 已实测

可用的导入相关端点

端点 用途 备注
POST /v3/product/import 创建产品 唯一可用的import端点
POST /v1/product/import/info 查询导入状态+审核结果 返回validation和moderation状态
POST /v1/product/import/prices 设置产品价格 ⚠️ 必须指定 currency_code: "CNY"
POST /v1/product/archive 归档产品 {"product_id": [123]}
POST /v3/product/info/list 批量获取产品详情 含分类和属性信息

废弃/404 的导入相关端点

端点 状态
POST /v1/product/import 404
POST /v2/product/import 404
POST /v2/products/stocks 404stock无法通过API设置)
POST /v1/product/update/stocks 404
所有 /v*/category/tree 404
所有 /v*/category/search 404
所有 /v*/category/attribute 404
所有 /v*/product/classify 404
所有 /v*/category/suggest 404

陷阱4:分类必须用 description_category_id + type_id

# ❌ 错误 — 使用 category_id
{"category_id": 12345}

# ✅ 正确 — 使用 description_category_id + type_id
{"description_category_id": 17028733, "type_id": 95421}

category_id 字段会报 "TypeId must be greater than 0"。必须用 description_category_id + type_id 组合。

获取分类的方法:从现有已审核通过的产品中提取(/v3/product/info/list 返回 description_category_idtype_id)。

陷阱5:属性格式必须用 id(不是 attribute_id

# ❌ 错误 — 使用 attribute_id
{"complex_id": 0, "attribute_id": 9048, "values": [{"value": "text"}]}

# ✅ 正确 — 使用 id
{"complex_id": 0, "id": 9048, "values": [{"value": "text"}]}

陷阱6:字典属性需要 dict_id

某些属性(如 attr 9163 "Пол"/性别)是字典类型,需要传 dict_idvalue

# ❌ 错误 — 只传 value 字符串
{"id": 9163, "values": [{"value": "унисекс"}]}

# ✅ 正确 — 传 dict_id + value
{"id": 9163, "values": [{"dict_id": 1, "value": "унисекс"}]}

但问题是:无法通过API查询字典值列表(所有attribute端点都404)。只能从已有产品的属性中反推,或试错。

陷阱7:价格API必须指定货币

# ❌ 错误 — 不指定货币或用 RUB
{"prices": [{"offer_id": "SKU1", "price": "5.99"}]}
# 报错:"Неверно указана валюта"(货币指定错误)

# ✅ 正确 — 指定 CNY
{"prices": [{"offer_id": "SKU1", "price": "5.99", "old_price": "9.99", "currency_code": "CNY"}]}

陷阱8:审核(Moderation)会检查图片与分类匹配

最重要的坑! 即使 validation: successOzon 的人工审核仍可能 declined 产品。

常见拒因:

  • "Фото товара не соответствует его типу" — 产品图片与分类类型不匹配
  • "Необходимо изменить тип" — 需要更改类型

这意味着:选错分类的产品100%会被审核拒绝,即使所有属性都填对了。必须找到与产品图片内容匹配的分类。

陷阱9:Stock 无法通过 API 设置

/v2/products/stocks 和所有 stock 相关端点均返回 404。库存可能需要在 Ozon Seller 后台手动设置,或通过 FBS/FBO 仓库管理。

完整产品导入代码模板

import requests
from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

OZON_H = {
    "Client-Id": "3103005",
    "Api-Key": "your-api-key",
    "Content-Type": "application/json"
}

# Step 1: Import product
import_payload = {
    "items": [{
        "name": "Солнцезащитные рукава для спорта",
        "offer_id": "SUNSLV668",
        "description": "Описание товара",
        "description_category_id": 17028733,  # ⚠️ 不是 category_id
        "type_id": 95421,
        "images": ["https://example.com/image.jpg"],
        "attributes": [
            {"complex_id": 0, "id": 9048, "values": [{"value": "текст"}]},  # ⚠️ 用 id 不是 attribute_id
        ],
        "vat": "0"
    }]
}

r = requests.post("https://api-seller.ozon.ru/v3/product/import",
                   headers=OZON_H, json=import_payload, timeout=15, verify=False)
result = r.json()
# result.result[0].product_id — 新产品ID
# result.result[0].errors — 导入错误列表

# Step 2: Check import status (repeat until status != "pending")
import time
task_id = result["result"][0]["task_id"]
for _ in range(20):
    r = requests.post("https://api-seller.ozon.ru/v1/product/import/info",
                      headers=OZON_H, json={"task_id": task_id}, timeout=15, verify=False)
    status = r.json()["result"]["items"][0]["status"]
    if status != "pending":
        break
    time.sleep(3)

# Step 3: Set price
r = requests.post("https://api-seller.ozon.ru/v1/product/import/prices",
    headers=OZON_H,
    json={"prices": [{
        "offer_id": "SUNSLV668",
        "price": "5.99",
        "old_price": "9.99",
        "currency_code": "CNY"  # ⚠️ 必须指定!
    }]},
    timeout=15, verify=False)

# Step 4: Check moderation result
product_id = result["result"][0]["product_id"]
r = requests.post("https://api-seller.ozon.ru/v3/product/info/list",
    headers=OZON_H,
    json={"product_id": [product_id]},
    timeout=15, verify=False)
product = r.json()["items"][0]
# product.status — "Не продается" = not for sale (declined)
# moderation — "declined" = 审核被拒

已知分类统计(从现有产品提取)

description_category_id type_id 名称 approved数 备注
17028733 95421 Декор/装饰品 35 通过率高但分类要匹配
17027904 93338 运动配件A 19 需性别属性(dict_id)
17027904 970575517 运动配件B 12 需性别属性(dict_id)
17027929 592744673 Ледоступы/冰爪 - 防晒袖被declined
17027904 93866 Солнцезащитные очки/太阳眼镜 - 运动防护类,可能适合防晒用品
41777465 93258 Солнцезащитная шляпа/太阳帽 - 4个必填字典属性,无法通过API填写

找正确分类的方法

  1. 从Ozon前台搜索:用Jina Reader抓取 ozon.ru/search/?text=关键词,找竞品的分类
  2. 从现有产品统计:用 /v3/product/info/list 获取所有产品,按 description_category_id 分组统计approved率
  3. AtomK前端编辑器:通过 atomlisting.com/editor/:id 选择Ozon平台,AI可能推荐正确分类
  4. 多分类并行测试:用同一产品数据同时提交到3-4个候选分类,看哪个validation先通过且无缺失属性

审核(Moderation)被拒的实战经验

即使 validation: success,审核仍可能 declined Validation 只检查格式,审核检查内容。

实测被拒记录:

  • 分类 17028733/95421(装饰品)→ "Необходимо изменить тип"(改类型)
  • 分类 17027929/592744673(冰爪)→ "Фото товара не соответствует его типу"(照片与类型不匹配)
  • 分类 41777465/93258(太阳帽)→ 4个必填字典属性无法填写,validation都无法通过

结论:分类名称必须与产品图片内容语义匹配。 Ozon审核员是真人,会看图片判断分类是否正确。"防晒袖"放在"冰爪"分类下100%被拒。

字典属性(dict_id)暴力破解法

当无法通过API查询字典值时(所有 /v*/category/attribute 端点404),可以暴力枚举 dict_id:

# 试错法找 dict_id — 从1开始递增
for dict_id in range(1, 200):
    test_attrs = [{"complex_id": 0, "id": 9163, "values": [{"dict_id": dict_id, "value": "унисекс"}]}]
    # 提交到 /v3/product/import,检查返回错误
    # 如果错误从 "attribute 9163 is missing" 变成别的,说明 dict_id 正确

实测结果(分类 17027904/970575517):

  • dict_id=1 → 通过import检查,但后续验证报错(不适用于此分类)
  • dict_id=49 → 通过import检查,但验证仍报错
  • 不同分类的字典值不同,一个分类下有效的dict_id在另一个分类下可能无效

offer_id 长度限制

# ❌ 错误 — offer_id 超过50字符
{"offer_id": "SUNSLV668G929X_17027904_970575517"}
# 报错:offer_id_invalid

# ✅ 正确 — offer_id 保持在50字符以内
{"offer_id": "SUNSLV668"}

产品生命周期状态流转

import → pending → validation:success/error → moderate:in_process → moderate:approved/declined
                                                    ↓
                                              如果approved → 需要stock+price → 在售
                                              如果declined → 必须改分类重新import(无法修改已declined产品的分类)

已declined的产品只能归档后重新导入到新分类。 无法修改已提交产品的分类。每次重新导入会生成新的 product_id。

归档大量测试产品的快捷方法

# 批量归档
requests.post("https://api-seller.ozon.ru/v1/product/archive",
    headers=OZON_H,
    json={"product_id": [4575426716, 4575556247, 4575793963, ...]},
    timeout=15)

v3/product/import/info 返回结构

{
  "result": {
    "items": [{
      "product_id": 4576116662,
      "status": "imported",     // pending / imported / failed
      "errors": [],
      "offer_id": "SUNSLV668"
    }],
    "task_id": 12345678
  }
}

注意:task_id 在 import 返回,用于查询状态。但也可以用 product_id 通过 /v3/product/info/list 查询最终状态。

依赖

无额外依赖,使用 Python requests 即可(标准库 urllib 也可)。