29 KiB
name, category, description
| name | category | description |
|---|---|---|
| atomk-api-export | cross-border-ecommerce | Extract product and listing data from AtomK via its undocumented REST API (JWT auth), then import into Miaoshou ERP collection box or other platforms. |
AtomK API Export & Integration Pipeline
Overview
AtomK (atomlisting.com) exposes an internal REST API that can be called directly with a JWT Bearer token. This is dramatically more efficient than browser-scraping for bulk export of claimed products and AI-generated listings.
Authentication
The API uses JWT Bearer tokens stored in browser localStorage after login. There is a second authentication method specifically for ERP integration, but agent nodes should only use Method 1 (JWT token via login).
Extract Token (DrissionPage)
token = page.run_js('return localStorage.getItem("atomk_token");')
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
'Accept': 'application/json',
}
Token payload fields: user_id, username, email, role, system_code, exp
Complete API Endpoint List (v1 — 2025-05 Upgrade)
认证 Auth
| 方法 | 端点 | 说明 |
|---|---|---|
| POST | /api/v1/auth/login | 登录获取JWT |
| POST | /api/v1/auth/refresh | 刷新token |
| GET | /api/v1/auth/verify-token | 验证token有效性 |
| GET | /api/v1/auth/me | 获取当前用户信息 |
产品 Products
| 方法 | 端点 | 说明 |
|---|---|---|
| GET | /api/v1/products/remote | 远程产品池列表 |
| GET | /api/v1/products/remote/random | 随机获取远程产品 |
| GET | /api/v1/products/remote/by-code/:code | 按产品编码查询 |
| GET | /api/v1/products/remote/categories | 远程产品分类 |
| GET | /api/v1/products/remote/:id | 远程产品详情 |
| POST | /api/v1/products/claim | 认领产品 |
| GET | /api/v1/products/claimed | 已认领产品列表 |
| GET | /api/v1/products/claimed/count | 已认领产品数量 |
| GET | /api/v1/products/claimed/categories | 已认领产品分类 |
| GET | /api/v1/products/claimed/:id | 已认领产品详情 |
| PUT | /api/v1/products/claimed/:id | 更新已认领产品 |
| DELETE | /api/v1/products/claimed/:id | 删除已认领产品 |
| POST | /api/v1/products/images/from-url | 从URL上传图片 |
| POST | /api/v1/products/images/from-file | 从文件上传图片 |
| DELETE | /api/v1/products/images/:id | 删除图片 |
Listing 上架
| 方法 | 端点 | 说明 |
|---|---|---|
| GET | /api/v1/listings | 列表(分页,max 100/page) |
| GET | /api/v1/listings/count | 数量统计 |
| POST | /api/v1/listings | 创建listing |
| GET | /api/v1/listings/:id | 详情 |
| PUT | /api/v1/listings/:id | 更新(含platform_fields) |
| DELETE | /api/v1/listings/:id | 删除 |
| POST | /api/v1/listings/generate | AI生成listing |
| POST | /api/v1/listings/:id/publish | 发布到平台 |
社交帖子 Social
| 方法 | 端点 | 说明 |
|---|---|---|
| GET | /api/v1/social/posts | 社交帖子列表 |
| POST | /api/v1/social/posts | 创建帖子 |
| GET | /api/v1/social/posts/:id | 帖子详情 |
| PUT | /api/v1/social/posts/:id | 更新帖子 |
| DELETE | /api/v1/social/posts/:id | 删除帖子 |
| POST | /api/v1/social/posts/generate | AI生成帖子 |
| POST | /api/v1/social/posts/:id/publish | 发布到社交平台 |
| GET | /api/v1/social/listings | 社交关联listing |
店铺 Stores (16 平台)
| 方法 | 端点 | 说明 |
|---|---|---|
| GET | /api/v1/stores | 所有店铺列表 |
| POST | /api/v1/stores | 创建店铺 |
| GET | /api/v1/stores/:id | 店铺详情 |
| PUT | /api/v1/stores/:id | 更新店铺 |
| DELETE | /api/v1/stores/:id | 删除店铺 |
| POST | /api/v1/stores/:id/test-connection | 测试连接 |
| GET | /api/v1/stores/:id/categories | 店铺分类 |
设置 Settings
| 方法 | 端点 | 说明 |
|---|---|---|
| GET | /api/v1/settings/accounts | 聚合:stores+social+erp+email |
| GET | /api/v1/settings/system-code | 公开,无需认证 |
| GET/POST/PUT/DELETE | /api/v1/settings/social-accounts | 社交账号 CRUD |
| GET/POST/PUT/DELETE | /api/v1/settings/erp-accounts | ERP账号 CRUD(10系统) |
| GET/POST/PUT/DELETE | /api/v1/settings/email-accounts | 邮箱账号 CRUD(10服务商) |
同步 Sync
| 方法 | 端点 | 说明 |
|---|---|---|
| POST | /api/v1/settings/sync/center-to-local | 中心→本地同步(需admin API key) |
| GET | /api/v1/settings/sync/status | 同步状态查询 |
Key Data Structures
Claimed Product Fields
| Field | Description |
|---|---|
id |
Internal AtomK product ID |
remote_product_code |
Product code (e.g., CNVGX8F4) |
local_sku |
Local SKU |
product_name |
English name |
product_name_zh |
Chinese name |
category |
Category (e.g., 家居百货) |
source_url |
Original 1688/source URL |
images |
List of COS image objects |
claimed_at |
ISO timestamp |
Image structure:
{
"id": 8230,
"image_url": "https://9websclub-1251422183.cos.ap-hongkong.myqcloud.com/AK-101/users/4/products/CNVGX8F4/0_0d3a9223.jpeg",
"cos_key": "AK-101/users/4/products/CNVGX8F4/0_0d3a9223.jpeg",
"is_primary": True,
"sort_order": 0
}
Listing Fields
| Field | Description |
|---|---|
id |
Listing ID |
claimed_product_id |
Links to product id |
platform |
temu, ozon, aliexpress, etc. |
title |
AI-generated title |
description |
AI-generated description |
price |
Price |
currency |
Currency code |
keywords |
Comma-separated keywords |
status |
generated or other |
ai_model |
hunyuan, deepseek, etc. |
csv_url |
Public COS URL to Temu-standard CSV |
html_url |
Public COS URL to formatted HTML product page |
platform_fields |
Nested dict with SKU, UPC, GTIN, EAN, package dims, image URLs, etc. |
created_at / updated_at |
ISO timestamps |
Platform fields (Temu example):
{
"SKU": "RC-MACARON-CANDLES-BOX360",
"ean": "7477596853977",
"upc": "747759886757",
"gtin": "0747759886757",
"price": 10.28,
"price_cny": 69.9,
"quantity": "360",
"exchange_rate": 6.8,
"package_width": "10",
"package_height": "2",
"package_length": "15",
"package_weight": "0.1",
"image_urls": "url1;url2;url3;...", # semicolon-separated COS URLs
"category_path": "Home & Garden > Party Supplies"
}
CSV Export (Pre-generated)
The csv_url is a publicly accessible COS link. No auth required to download:
import requests
csv_resp = requests.get(listing['csv_url'], timeout=30)
# Returns UTF-8 BOM CSV with Temu-standard columns
HTML Export (Pre-generated)
The html_url is also public. Self-contained, styled HTML product detail page:
html_resp = requests.get(listing['html_url'], timeout=30)
Integration with Miaoshou ERP 采集箱
Miaoshou Import Collection Box URL
https://erp.91miaoshou.com/common_collect_box/index?fetchType=importCopy
Import Methods
- 导入链接采集 (Import Link Collection) — requires xls with source URLs
- Excel表格导入 (Excel Import) — for bulk data
- 本地素材包导入 (Local Material Package Import)
Import Link Collection Template
Downloaded template: 导入产品链接模板.xls
Columns:
| Column | Field | Required |
|---|---|---|
| A | 链接地址(必填) | ✅ Yes |
| B | 产品标题 | Optional |
| C | 价格(RMB) | Optional |
| D | 促销价(RMB) | Optional (Lazada only) |
How Miaoshou processes it: For each link, Miaoshou's scraper visits the URL and extracts product info (images, title, description). Optional columns override scraped values.
Workflow: AtomK → Miaoshou (Link Import) — Validated ✅
CRITICAL: Miaoshou's scraper only supports 1688/Taobao/Pinduoduo URLs. AtomK's html_url (COS-hosted HTML) will always fail with "解析失败". You must use the original source_url from /api/v1/products/claimed.
import requests
import xlwt
requests.packages.urllib3.disable_warnings()
# 1. Login AtomK
login_resp = requests.post('https://www.atomlisting.com/api/v1/auth/login',
json={'username': 'admincao', 'password': 'Tt123456!'},
timeout=10, verify=False)
token = login_resp.json()['token']
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}
# 2. Get claimed products (for source_url) AND listings (for title/price)
products = requests.get('https://www.atomlisting.com/api/v1/products/claimed?page=1&limit=100',
headers=headers, timeout=30, verify=False).json()
listings = requests.get('https://www.atomlisting.com/api/v1/listings',
headers=headers, timeout=30, verify=False).json()
# 3. Match listing → product via claimed_product_id to get 1688 source_url
matched = []
for listing in listings:
if not listing.get('html_url'): # skip incomplete
continue
product = next((p for p in products if p['id'] == listing['claimed_product_id']), None)
if product and product.get('source_url'):
matched.append({'listing': listing, 'product': product})
# 4. Build Miaoshou import xls using OFFICIAL template format
workbook = xlwt.Workbook()
sheet = workbook.add_sheet('Worksheet')
# Official columns (verified by downloading template from Miaoshou UI):
sheet.write(0, 0, '链接地址(必填)')
sheet.write(0, 1, '产品标题')
sheet.write(0, 2, '价格(RMB)')
sheet.write(0, 3, '促销价(RMB)')
sheet.write(0, 4, '提示:促销价仅可用于采集到Lazada')
for idx, item in enumerate(matched, start=1):
listing = item['listing']
product = item['product']
pf = listing.get('platform_fields', {})
sheet.write(idx, 0, product['source_url']) # ✅ 1688 link, NOT html_url
sheet.write(idx, 1, listing['title'])
sheet.write(idx, 2, pf.get('price_cny', ''))
sheet.write(idx, 3, '')
sheet.write(idx, 4, '')
workbook.save('/tmp/miaoshou_import.xls')
5. Upload to Miaoshou via browser automation:
# Prerequisites: logged into Miaoshou via DrissionPage (see captcha-auto-login skill)
page.get('https://erp.91miaoshou.com/common_collect_box/index?fetchType=importCopy')
time.sleep(8)
# Step 1: Close notification popups (critical — they block interactions)
for _ in range(5):
page.run_js('''
var btns = document.querySelectorAll("button, .jx-dialog__close, .jx-dialog__headerbtn");
for (var i = 0; i < btns.length; i++) {
var text = btns[i].textContent.trim();
var ariaLabel = btns[i].getAttribute('aria-label') || '';
var cls = btns[i].className || '';
if (text === "关闭" || text === "我知道了" || ariaLabel.includes("关闭")
|| cls.includes('close') || cls.includes('headerbtn')) {
btns[i].click();
}
}
''')
time.sleep(2)
# Step 2: Open import dialog
page.run_js('''
var btns = document.querySelectorAll("button");
for (var i = 0; i < btns.length; i++) {
if (btns[i].textContent.trim().includes("导入链接采集")) {
var evt = new MouseEvent("click", { bubbles: true, cancelable: true, view: window });
btns[i].dispatchEvent(evt);
return true;
}
}
return false;
''')
time.sleep(8)
# Step 3: Upload file via hidden input
file_input = page.ele('css:.jx-upload__input', timeout=5)
file_input.input('/tmp/miaoshou_import.xls')
time.sleep(5)
# Step 4: Click confirm
page.run_js('''
var modals = document.querySelectorAll(".jx-dialog");
for (var i = 0; i < modals.length; i++) {
var style = window.getComputedStyle(modals[i]);
if (style.display !== "none") {
var btns = modals[i].querySelectorAll("button");
for (var j = 0; j < btns.length; j++) {
if (btns[j].textContent.trim() === "确认") {
btns[j].click();
return true;
}
}
}
}
return false;
''')
# Step 5: Wait for parsing (1-5 minutes)
time.sleep(180)
# Status will show: 等待解析 → 解析成功 (X/Y/Z) or 解析失败
Expected result: 解析成功 3/0/3 (all succeeded) — NOT 解析失败.
If it shows "等待解析": Wait 1-5 minutes then refresh. If it shows "解析失败 解析文件数据错误", the template format is wrong. If it shows "导入文件未识别到:【产品主编号】表头", you're using the wrong import method or an outdated template.
Alternative: Full Data Injection (Bypass Scraping)
If link scraping fails, use the complete data from platform_fields to directly create products in Miaoshou or target platforms:
- Title, description, price, quantity, SKU, UPC, GTIN, EAN
- Package dimensions & weight
- Image URLs (semicolon-separated COS links)
- Keywords
This bypasses Miaoshou's collection box entirely and goes straight to platform-specific CSV generation.
Pitfalls
- ✅ CRITICAL:
html_urldoes NOT work with Miaoshou — Miaoshou's scraper only supports 1688/Taobao/Pinduoduo. You MUST usesource_urlfrom/api/v1/products/claimed(original 1688 link). Usinghtml_urlproduces解析失败 0/3/3100% of the time. - Wrong template = "产品主编号" error — If you see "导入文件未识别到:【产品主编号】表头", you are either using the wrong import method (e.g., "Excel表格导入" instead of "导入链接采集") or an outdated template. The correct "导入链接采集" template has only:
链接地址(必填), 产品标题, 价格(RMB), 促销价(RMB). - Notification popups block UI — Miaoshou shows "我知道了" notification popups that MUST be closed before clicking "导入链接采集". Failure to close them causes the click to do nothing.
- Sub-account permission limits — The account
xiaochaoren2026is a Miaoshou sub-account. It can access采集箱/导入采集but shows "暂无当前模块权限" on product publish/edit pages. Use a main account for full publishing. - 店小秖 (Dianxiaomi) requires slider CAPTCHA — Login page has "拖动下方拼图完成验证" which is extremely difficult to bypass automatically. Prefer Miaoshou for automation.
- API is partially documented — 2025-05 upgrade added many endpoints. Some new endpoints may have undocumented body fields or response shapes. Monitor 404/500 responses.
- Token expiry — JWT has
expfield. UsePOST /api/v1/auth/refreshwith refresh token if needed. - Image format mix — AtomK stores both
.jpegand.webpimages. Some platforms prefer.jpegonly. UsePOST /api/v1/products/images/from-urlorfrom-filefor image management. - COS URLs are public — but tied to AtomK's Tencent Cloud account. If AtomK changes storage policy, URLs may break.
- No batch export API —
/api/v1/exportdoes not exist. Export is done product-by-product via pre-generatedcsv_url/html_url. - ERP accounts should NOT be stored in Stores — use
/api/v1/settings/erp-accountsinstead. The old workaround ofplatform=woocommerce+extra_data.real_platform=miaoshou_erpis deprecated. POST /settings/erp-accountshas 500 bug (2025-05) — all creation attempts fail regardless of platform. Store ERP credentials in local file (~/.hermes/credentials/accounts.md) until backend is fixed.passwordinextra_dataauto-filtered to***— always usepwdfield for any account type (stores, ERP, email).- Silent UI Failures for Account Linking —
POST /api/v1/storesmay return 502 (telnumber DB constraint),POST /api/v1/settings/social-accountsmay return 500 for unsupported platforms. Intercept requests to debug. - Social platform whitelist — only
facebook,tiktok,twitter,wordpresswork.instagram,pinterestreturn 500. - Sync API needs API key —
GET /settings/sync/statusandPOST /settings/sync/center-to-localrequirex-api-keyheader, not JWT. status="imported"triggers 500 — always usestatus="generated"when creating listings.platform_fieldsis silently ignored in POST — you must use PUT to set it after creation.claimed_product_idmust be unique — assigning the same product ID to multiple listings creates duplicates.- API returns max 100 listings per page — use
pageparameter to paginate. - 502 Bad Gateway on rapid fire — add 50ms delay between requests, or implement retry with backoff.
- Consecutive 500s mean a bad product — if you get 10 consecutive 500s, skip that product. The product data itself is likely invalid.
- Deduplicate before creating — always check existing listings first. Re-running a sync script without dedup creates duplicates.
POST /api/v1/settings/sync/center-to-localrequires admin API key, not regular JWT.- Ozon Seller API keys expire — both Hzqjone(client_id=3098640) and Hzqjtwo(client_id=3103005) keys returned "Invalid Api-Key" as of 2025-05. Must regenerate from seller.ozon.ru (requires Russian IP). Ozon product import uses
/v3/product/import(not/v1/). - Ozon publish not supported —
POST /listings/:id/publishreturns 400 for Ozon. Must use Ozon Seller API directly to create products.
Stores API Details
Working platform values: walmart, temu, ozon, aliexpress, amazon, ebay, woocommerce, lazada, shopee
Store fields:
| Field | Description |
|---|---|
id |
Store ID |
platform |
E-commerce platform (strict validation) |
store_name |
Human-readable name |
store_url |
Platform URL |
extra_data |
Arbitrary JSON (API keys, credentials, etc.) |
is_active |
Boolean |
Pitfalls for Stores API:
passwordinextra_dataauto-filtered to***— usepwdfield instead.- Platform validation is strict —
miaoshou,shopify,other,erpall return 500. Only e-commerce platforms accepted. - ERP accounts do NOT belong in Stores — use
/api/v1/settings/erp-accountsinstead. POST /stores/:id/test-connection— validates store credentials (API key, URL, etc.)GET /stores/:id/categories— fetches platform category tree (not all platforms support this; walmart returns 400)
Settings API Details
GET /api/v1/settings/accounts — 聚合所有账号信息(stores + social + erp + email),一站式获取。
GET /api/v1/settings/system-code — 公开端点,无需认证。返回 {"system_code": "AK-101"}。
ERP Accounts CRUD (/api/v1/settings/erp-accounts):
- 必填字段:
erp_name,account_name - 可选字段:
platform,extra_data,is_active - ⚠️ POST currently returns 500 (backend bug, 2025-05) — all platform values fail. Workaround: store ERP credentials in
/home/ubuntu/.hermes/credentials/accounts.mduntil backend is fixed. - When fixed, expected usage:
erp = {
"erp_name": "妙手ERP",
"platform": "miaoshou",
"account_name": "17762501033",
"extra_data": {"phone": "17762501033", "pwd": "Tt123456!"},
"is_active": True
}
requests.post(f'{BASE}/settings/erp-accounts', json=erp, headers=headers)
Email Accounts CRUD (/api/v1/settings/email-accounts):
- 必填字段:
email_provider,email_address,account_name - 可选字段:
smtp_host,smtp_port,smtp_username,use_tls,extra_data,is_active,linked_store_id - 密码存
extra_data.pwd(不要用password)
email = {
"email_provider": "gmail",
"email_address": "admin@9webs.cn",
"account_name": "9webs Admin",
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"smtp_username": "admin@9webs.cn",
"use_tls": True,
"extra_data": {"pwd": "xxx"},
"is_active": True
}
Social Accounts CRUD (/api/v1/settings/social-accounts):
- 字段:
platform,account_name,account_handle,linked_store_id,is_active - 可用platform:
facebook,tiktok,twitter,wordpress - ⚠️
instagram,pinterest返回 500 (backend bug)
Social Posts API
Social模块管理社交帖子(Facebook、Twitter等),结构类似Listings:
GET /social/posts— 帖子列表POST /social/posts/generate— AI生成社交帖子内容POST /social/posts/:id/publish— 发布到社交平台GET /social/listings— 获取与社交帖子关联的listing数据
Social Post fields:
| Field | Description |
|---|---|
id |
Post ID |
platform |
Social platform |
content |
Post content text |
images |
Image URLs |
status |
draft / generated / published |
linked_listing_id |
Optional link to a listing |
Sync API Details
POST /api/v1/settings/sync/center-to-local:
- 从中心数据库同步到本地实例
- ⚠️ 需要 admin API key(
x-api-keyheader),非JWT token。普通JWT返回401。 - 用于多实例部署场景
GET /api/v1/settings/sync/status:
- 查询当前同步状态
- 同样需要 API key
Listings Generate & Publish
POST /api/v1/listings/generate — AI生成listing数据:
- 请求体:
{"claimed_product_id": 123, "platform": "ozon", "store_id": 12} - 自动生成: 目标语言标题/描述/关键词(Ozon=俄语)、EAN/UPC/GTIN条码、定价(汇率6.8换算)、包裹尺寸、分类路径
- 耗时约30-60秒/个,需设timeout=120+
- 返回完整listing对象(含id/title/description/platform_fields/csv_url/html_url等)
- 批量生成时需逐个调用,不能并行。每个generate是同步的(不是async)
POST /api/v1/listings/:id/publish — 推送到目标平台:
- 前提: store已配置且
test-connection通过 - ⚠️ Ozon不支持 — 返回400 "Platform ozon publishing not supported yet"
- Walmart/Temu/AliExpress可能支持(未测试)
- Ozon上架需走Ozon Seller API (
/v3/product/import) 直接推
批量Generate模式 (推荐):
# 逐个generate,带timeout和重试
import requests, time
s = requests.Session()
s.verify = False
# ... login ...
# 获取未使用的claimed products
products = [] # 从 /products/claimed 获取
used = set() # 从 /listings 获取已用的claimed_product_id
unused = [p for p in products if p['id'] not in used]
for i, prod in enumerate(unused[:N]):
try:
r = s.post(f'{BASE}/listings/generate', json={
'claimed_product_id': prod['id'],
'platform': 'ozon',
'store_id': 12,
}, timeout=120)
if r.status_code == 200:
listing = r.json()
print(f"[{i+1}] OK id={listing['id']} title={listing.get('title','')[:40]}")
else:
print(f"[{i+1}] FAIL {r.status_code}: {r.text[:100]}")
except requests.exceptions.ReadTimeout:
print(f"[{i+1}] TIMEOUT (product {prod['id']} may be too complex)")
time.sleep(0.5)
Verification Steps
- Confirm
api/v1/listingsreturns expected data shape andclaimed_product_idmatchesapi/v1/products/claimeditems. - Confirm
source_urlexists on claimed products and points todetail.1688.com. - Generate a 1-row test XLS with official template format (
链接地址(必填), 产品标题, 价格(RMB)) usingsource_url. - Upload to Miaoshou via "导入链接采集". Wait 1-5 minutes. Expected:
解析成功 1/0/1. - If you see
解析失败, check: (a) URL is 1688 not AtomK HTML, (b) template downloaded from Miaoshou UI is current, (c) popup notifications were closed before upload.
Listings API — Write Side (Create / Update / Delete)
Create Listing
POST https://www.atomlisting.com/api/v1/listings
Required body fields:
{
"claimed_product_id": 123, # must be a valid, unassigned claimed product ID
"platform": "ozon", # platform name
"store_id": 12, # store ID from /api/v1/stores
"title": "Product Title",
"description": "Description",
"price": 9.99,
"currency": "CNY",
"status": "generated" # NOT "imported" — that causes 500!
}
Pitfalls for Create:
status="imported"triggers 500 — always usestatus="generated".platform_fieldsis silently ignored in POST — you must use PUT to set it after creation.claimed_product_idmust be unique — assigning the same product ID to multiple listings creates duplicates. Track which product IDs are already used.- API pagination limit is 100 —
GET /listings?limit=100&page=N.limit>100returns empty items.
Update Listing (Set platform_fields)
PUT https://www.atomlisting.com/api/v1/listings/{id}
{
"platform_fields": {
"product_id": "123456789",
"offer_id": "SKU-001",
"store_name": "Hzqjone",
"url": "https://www.ozon.ru/product/123456789"
}
}
Delete Listing
DELETE https://www.atomlisting.com/api/v1/listings/{id}
Bulk Sync Pattern (Ozon → AtomK)
When syncing external platform listings (Ozon, Temu, etc.) into AtomK, use this pattern:
import requests, json, time
from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
BASE = 'https://www.atomlisting.com/api/v1'
s = requests.Session()
s.verify = False
# 1. Login
r = s.post(f'{BASE}/auth/login', json={'username': 'admincao', 'password': 'Tt123456!'}, timeout=10)
s.headers.update({'Authorization': f'Bearer {r.json()["token"]}'})
# 2. Fetch ALL existing listings with pagination (max 100 per page)
all_listings = []
page = 1
while True:
r = s.get(f'{BASE}/listings', params={'page': page, 'limit': 100}, timeout=15)
d = r.json()
items = d if isinstance(d, list) else d.get('items', [])
all_listings.extend(items)
if len(items) < 100:
break
page += 1
# 3. Build index: product_id -> [listing_ids] for dedup
ozon_listings = [l for l in all_listings if l.get('platform') == 'ozon']
by_pid = {}
for l in ozon_listings:
pf = l.get('platform_fields') or {}
pid = pf.get('product_id')
if pid:
by_pid.setdefault(pid, []).append(l)
# 4. Delete duplicates (keep first, delete rest)
for pid, lst in by_pid.items():
for l in lst[1:]:
s.delete(f'{BASE}/listings/{l["id"]}', timeout=10)
time.sleep(0.05)
# 5. Get unused claimed_product_ids
products = []
page = 1
while True:
r = s.get(f'{BASE}/products/claimed', params={'page': page, 'limit': 100}, timeout=15)
items = r.json() if isinstance(r.json(), list) else r.json().get('items', [])
products.extend(items)
if len(items) < 100: break
page += 1
used_pids = {l['claimed_product_id'] for l in ozon_listings}
unused = [p['id'] for p in products if p['id'] not in used_pids]
# 6. Create missing listings
existing_pids = set(by_pid.keys())
for i, product_data in enumerate(external_products):
if product_data['product_id'] in existing_pids:
continue
if not unused:
print("No more unused claimed_product_ids!")
break
cp_id = unused.pop(0)
# POST to create (without platform_fields)
r = s.post(f'{BASE}/listings', json={
'claimed_product_id': cp_id,
'platform': 'ozon',
'store_id': store_id,
'title': product_data.get('name', f'Ozon-{product_data["product_id"]}'),
'description': '',
'price': product_data.get('price', 0),
'currency': 'CNY',
'status': 'generated',
}, timeout=10)
if r.status_code == 201:
listing_id = r.json()['id']
# PUT to set platform_fields
s.put(f'{BASE}/listings/{listing_id}', json={
'platform_fields': {
'product_id': str(product_data['product_id']),
'offer_id': product_data.get('offer_id', ''),
'store_name': store_name,
'url': f'https://www.ozon.ru/product/{product_data["product_id"]}',
}
}, timeout=10)
time.sleep(0.05) # rate limit protection
Critical Pitfalls for Bulk Sync:
- Pagination is mandatory —
/listingsreturns max 100 items. Always paginate or you'll create hundreds of duplicates. - Two-step creation — POST creates the listing, but
platform_fieldsmust be set via a separate PUT. The POST silently ignoresplatform_fields. status="imported"= 500 — use"generated".- 502 Bad Gateway on rapid fire — add 50ms delay between requests, or implement retry with backoff.
- Consecutive 500s mean a bad product, not rate limiting — if you get 10 consecutive 500s, skip that product. The product data itself is likely invalid (e.g., null fields the backend can't handle).
- Deduplicate before creating — always check existing listings first. Re-running a sync script without dedup creates 2x, 3x duplicates.
References
references/backend-development.md— Full guide for adding new API endpoints: model creation, router setup, Alembic migration, production deployment (no CI/CD, manual SSH), plus a debug pattern for Desktop-backend API mismatch errors.references/billionmail-api.md— BillionMail (SG4) email server API reference: mailbox creation, SMTP relay, and default domain (9webs.site).
Related Skills
atomk-temu-batch-listing— browser-based batch claiming and AI generationmiaoshou-collect-box— Miaoshou ERP collection box navigationcaptcha-auto-login— automated ERP login with CAPTCHA solving