关于 marketplace-catalog-sync
面向 VTEX 外部卖家的目录集成技能,指导实现 SKU 通知、建议审批以及价格和库存同步,并强调正确路由、限流与状态处理。
主要功能
- 01区分卖家 SKU 与市场 SKU 路由
- 02实现 SKU 建议与审批生命周期
- 03同步价格和库存变更
- 04处理 429 限流与重试
- 05满足履约模拟时限要求
适用场景
- 01构建卖家目录连接器
- 02注册新 SKU 并跟踪审批
- 03向市场通知价格和库存变化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vtex/ skills --skill marketplace-catalog-sync安装后在 Claude Code 中输入/marketplace-catalog-sync确认已加载。
详细介绍
目录与 SKU 集成
何时使用此技能
构建卖家连接器时,如果需要将产品目录数据推送到 VTEX marketplace、处理 SKU 审核工作流,或保持价格与库存同步,请使用此技能。
- 构建用于注册和更新 SKU 的变更通知流程
- 实现 SKU 建议生命周期(发送 → 待处理 → 已批准/已拒绝)
- 将产品数据映射到 VTEX 目录架构
- 通过通知端点同步价格与库存
请勿将此技能用于:
- Marketplace 端的目录操作(直接写入 Catalog API)
- 订单履行或发票处理(请参阅
marketplace-fulfillment) - 单独处理限流模式(请参阅
marketplace-rate-limiting)
决策规则
VTEX 在 api/catalog_system/pvt/skuseller/changenotification 下提供了两个 POST 路由。它们不能互换使用——路径形式决定了平台如何识别你提供的标识符。
| 路由 | 路径模式 | 含义 |
|---|---|---|
| 带 marketplace SKU ID 的变更通知 | .../changenotification/{skuId} | {skuId} 是marketplace 目录中的 SKU ID(VTEX)。URL 中没有 sellerId。 |
| 带卖家 ID 和卖家 SKU ID 的变更通知 | .../changenotification/{sellerId}/{skuId} | {sellerId} 是 marketplace 上的卖家账户;{skuId} 是卖家自己的 SKU 代码(与 PUT SKU Suggestion 路径中使用的 ID 相同)。 |
卖家连接器集成必须使用第二个路由。官方文档有时会混淆这两个路由的描述——应以URL 形式为准:卖家范围的流程在 changenotification/ 后始终使用两个路径段。
SKU Suggestion(PUT/GET)必须发送到 https://api.vtex.com/{accountName}/suggestions/{sellerId}/{sellerSkuId},而不是店铺主机名。App Key 和 App Token 相同。不要使用 {account}.vtexcommercestable.com.br/api/catalog_system/pvt/sku/seller/... 构建 suggestion URL——那是另一套 Catalog System 接口。
- 对于卖家侧目录集成,使用
POST /api/catalog_system/pvt/skuseller/changenotification/{sellerId}/{sellerSkuId}(marketplace 账户中的卖家 ID + 卖家 SKU ID)。200 OK 表示该 SKU 已存在于该卖家在 marketplace 中的目录中(更新流程);404 Not Found 表示不存在(发送 SKU Suggestion)。不要使用POST .../changenotification/{skuId}搭配卖家的 SKU 代码——该单段路由要求提供marketplace SKU ID。 - 仅当你拥有的标识符是 VTEX marketplace SKU ID 时,才使用
POST .../changenotification/{skuId}(路径中没有卖家段)。 - 使用
https://api.vtex.com/{accountName}下的/suggestions/{sellerId}/{sellerSkuId}执行PUT(Send SKU Suggestion),以注册或更新待处理的 suggestion。卖家不拥有目录——每个新 SKU 都必须经过 suggestion/审批工作流。 - 价格和库存使用单独的通知端点:
POST /notificator/{sellerId}/changenotification/{sellerSkuId}/price和POST /notificator/{sellerId}/changenotification/{sellerSkuId}/inventory。changenotification/后的路径段是卖家 SKU ID(卖家自己的 SKU 代码——与 suggestion 和卖家范围的目录流程中使用的标识符相同),而不是 marketplace VTEX SKU ID。参考文档可能将此路径段标记为skuId;在卖家连接器集成中,应将其理解为 sellerSkuId。 - 收到价格/库存通知后,marketplace 会调用卖家的 Fulfillment Simulation 端点(
POST /pvt/orderForms/simulation)。此端点必须在 2.5 秒内响应,否则该商品会被视为不可用。 - 只有处于 “pending” 状态时才能更新 suggestion。获批或被拒后,卖家无法再修改。
架构/数据流:
Seller VTEX Marketplace
│ │
│─── POST changenotification ──────▶│
│◀── 200 (exists) or 404 (new) ────│
│ │
│─── PUT Send SKU Suggestion ──────▶│ (if 404)
│ │── Pending in Received SKUs
│ │── Marketplace approves/denies
│ │
│─── POST price notification ──────▶│
│◀── POST fulfillment simulation ───│ (marketplace fetches data)
│─── Response with price/stock ────▶│
硬性限制
约束:使用 SKU 集成 API,而非直接使用 Catalog API
外部卖家 MUST 使用变更通知 + SKU 建议流程来集成 SKU。直接写入 Catalog API(POST /api/catalog/pvt/product 或 POST /api/catalog/pvt/stockkeepingunit)仅用于 marketplace 侧的操作。
原因
卖家不拥有目录。直接写入目录会因 403 Forbidden 而失败,或创建绕过审批流程的孤立条目。建议机制可确保 marketplace 进行质量控制。
检测
如果发现卖家集成直接调用 Catalog API 创建商品/SKU(例如 POST /api/catalog/pvt/product、POST /api/catalog/pvt/stockkeepingunit)→ 提醒应改用 SKU 集成 API。
正确
import axios, { AxiosInstance } from "axios";
interface SkuSuggestion {
ProductName: string;
SkuName: string;
ImageUrl: string;
ProductDescription: string;
BrandName: string;
CategoryFullPath: string;
EAN: string;
Height: number;
Width: number;
Length: number;
WeightKg: number;
SkuSpecifications: Array<{
FieldName: string;
FieldValues: string[];
}>;
}
async function integrateSellerSku(
client: AxiosInstance,
marketplaceAccount: string,
sellerId: string,
sellerSkuId: string,
skuData: SkuSuggestion
): Promise<void> {
const storeBaseUrl = `https://${marketplaceAccount}.vtexcommercestable.com.br`;
const suggestionUrl = `https://api.vtex.com/${marketplaceAccount}/suggestions/${sellerId}/${sellerSkuId}`;
// Step 1: Seller-scoped change notification (Catalog API — store host)
try {
await client.post(
`${storeBaseUrl}/api/catalog_system/pvt/skuseller/changenotification/${sellerId}/${sellerSkuId}`
);
// 200 OK — SKU exists, marketplace will fetch updates via fulfillment simulation
console.log(`SKU ${sellerSkuId} exists in marketplace, update triggered`);
} catch (error: unknown) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
// 404 — SKU not found, send suggestion
console.log(`SKU ${sellerSkuId} not found, sending suggestion`);
await client.put(suggestionUrl, skuData);
console.log(`SKU suggestion sent for ${sellerSkuId}`);
} else {
throw error;
}
}
}
错误
// WRONG: Marketplace-SKU-only path with the seller's SKU code (misroutes the notification).
// .../changenotification/{skuId} expects the VTEX marketplace SKU ID, not sellerSkuId.
await client.post(
`https://${marketplaceAccount}.vtexcommercestable.com.br/api/catalog_system/pvt/skuseller/changenotification/${sellerSkuId}`
);
// WRONG: SKU Suggestion on the store host + Catalog path — public contract is api.vtex.com + /suggestions/...
await client.put(
`https://${marketplaceAccount}.vtexcommercestable.com.br/api/catalog_system/pvt/sku/seller/${sellerId}/suggestion/${sellerSkuId}`,
skuData
);
// WRONG: Seller writing directly to marketplace catalog — bypasses suggestion/approval; expect 403
async function createSkuDirectly(
client: AxiosInstance,
marketplaceAccount: string,
productData: Record<string, unknown>
): Promise<void> {
// Direct catalog write — sellers don't have permission for this
await client.post(
`https://${marketplaceAccount}.vtexcommercestable.com.br/api/catalog/pvt/product`,
productData
);
// Will fail: 403 Forbidden — seller lacks catalog write permissions
// Will fail: 403 Forbidden — seller lacks catalog write permissions
}
约束:处理目录通知的速率限制
所有目录通知调用 MUST 实现 429 处理,并采用指数退避。批量通知 MUST 经过限流,以遵守 VTEX API 速率限制。
原因
变更通知端点设有速率限制。批量发送通知而不进行限流会触发 429 响应,并暂时阻止卖家访问 API,导致整个集成停滞。
检测
如果发现目录通知调用没有处理 429 或重试逻辑 → STOP 并添加限流。如果发现通过紧密循环发送通知且没有延迟 → 提醒存在速率限制问题。
正确
async function batchNotifySkus(
client: AxiosInstance,
baseUrl: string,
sellerId: string,
sellerSkuIds: string[],
concurrency: number = 5,
delayMs: number = 200
): Promise<void> {
const results: Array<{ sellerSkuId: string; status: "exists" | "new" | "error" }> = [];
for (let i = 0; i < sellerSkuIds.length; i += concurrency) {
const batch = sellerSkuIds.slice(i, i + concurrency);
const batchResults = await Promise.allSettled(
batch.map(async (sellerSkuId) => {
try {
await client.post(
`${baseUrl}/api/catalog_system/pvt/skuseller/changenotification/${sellerId}/${sellerSkuId}`
);
return { sellerSkuId, status: "exists" as const };
} catch (error: unknown) {
if (
error instanceof Error &&
"response" in error &&
(error as { response?: { status?: number } }).response?.status === 404
) {
return { sellerSkuId, status: "new" as const };
}
if (
error instanceof Error &&
"response" in error &&
(error as { response?: { status?: number; headers?: Record<string, string> } })
.response?.status === 429
) {
const retryAfter = parseInt(
(error as { response: { headers: Record<string, string> } }).response.headers[
"retry-after"
] || "60",
10
);
console.warn(`Rate limited. Waiting ${retryAfter}s before retry.`);
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
return { sellerSkuId, status: "error" as const };
}
throw error;
}
})
);
for (const result of batchResults) {
if (result.status === "fulfilled") {
results.push(result.value);
}
}
// Throttle between batches
if (i + concurrency < sellerSkuIds.length) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
}
错误
// WRONG: No rate limiting, no error handling, tight loop
async function notifyAllSkus(
client: AxiosInstance,
baseUrl: string,
sellerId: string,
sellerSkuIds: string[]
): Promise<void> {
// Fires all requests simultaneously — will trigger 429 rate limits
await Promise.all(
sellerSkuIds.map((sellerSkuId) =>
client.post(
`${baseUrl}/api/catalog_system/pvt/skuseller/changenotification/${sellerId}/${sellerSkuId}`
)
)
);
}
约束:处理建议生命周期状态
卖家 MUST 在尝试更新前检查建议状态。只有处于待处理状态的建议才能更新。
原因
尝试更新已批准或已拒绝的建议会静默失败,或创建重复条目。建议获批后将成为 marketplace 所有的 SKU。
检测
如果发现更新 SKU 建议时没有检查当前建议状态 → 提醒需要处理建议生命周期。
正确
async function updateSkuSuggestion(
client: AxiosInstance,
marketplaceAccount: string,
sellerId: string,
sellerSkuId: string,
updatedData: Record<string, unknown>
): Promise<boolean> {
const suggestionUrl = `https://api.vtex.com/${marketplaceAccount}/suggestions/${sellerId}/${sellerSkuId}`;
// Check current suggestion status before updating
try {
const response = await client.get(suggestionUrl);
const suggestion = response.data;
if (suggestion.Status === "Pending") {
// Safe to update — suggestion hasn't been processed yet
await client.put(suggestionUrl, updatedData);
return true;
}
// Already approved or denied — cannot update
console.warn(
`SKU ${sellerSkuId} suggestion is ${suggestion.Status}, cannot update. ` +
`Use changenotification to update existing SKUs.`
);
return false;
} catch {
// Suggestion may not exist — send as new
await client.put(suggestionUrl, updatedData);
return true;
}
}
错误
// WRONG: Blindly sending suggestion update without checking state
async function blindUpdateSuggestion(
client: AxiosInstance,
marketplaceAccount: string,
sellerId: string,
sellerSkuId: string,
data: Record<string, unknown>
): Promise<void> {
// If the suggestion was already approved, this fails silently
// or creates a duplicate that confuses the marketplace operator
await client.put(
`https://api.vtex.com/${marketplaceAccount}/suggestions/${sellerId}/${sellerSkuId}`,
data
);
}
首选模式
设置卖家连接器客户端
创建经过身份验证的 HTTP 客户端,以便与 VTEX marketplace 通信。
import axios, { AxiosInstance } from "axios";
interface SellerConnectorConfig {
marketplaceAccount: string;
sellerId: string;
appKey: string;
appToken: string;
}
function createMarketplaceClient(config: SellerConnectorConfig): AxiosInstance {
return axios.create({
// Catalog System routes (e.g. changenotification) use the store host.
baseURL: `https://${config.marketplaceAccount}.vtexcommercestable.com.br`,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-VTEX-API-AppKey": config.appKey,
"X-VTEX-API-AppToken": config.appToken,
},
timeout: 10000,
});
}
// Use the same headers for PUT/GET on https://api.vtex.com/{account}/suggestions/...
// (pass a full URL on the same axios instance, or set baseURL to https://api.vtex.com/{account} for suggestion-only calls).
实现变更通知流程
处理 changenotification 端点返回的“已存在”(200)和“新增”(404)两种情况。
interface CatalogNotificationResult {
skuId: string;
action: "updated" | "suggestion_sent" | "error";
error?: string;
}
async function notifyAndSync(
client: AxiosInstance,
marketplaceAccount: string,
sellerId: string,
sellerSkuId: string,
skuData: SkuSuggestion
): Promise<CatalogNotificationResult> {
const suggestionUrl = `https://api.vtex.com/${marketplaceAccount}/suggestions/${sellerId}/${sellerSkuId}`;
try {
await client.post(
`/api/catalog_system/pvt/skuseller/changenotification/${sellerId}/${sellerSkuId}`
);
// SKU exists — marketplace will call fulfillment simulation to get updates
return { skuId: sellerSkuId, action: "updated" };
} catch (error: unknown) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
try {
await client.put(suggestionUrl, skuData);
return { skuId: sellerSkuId, action: "suggestion_sent" };
} catch (suggestionError: unknown) {
const message = suggestionError instanceof Error ? suggestionError.message : "Unknown error";
return { skuId: sellerSkuId, action: "error", error: message };
}
}
const message = error instanceof Error ? error.message : "Unknown error";
return { skuId: sellerSkuId, action: "error", error: message };
}
}
实现履约模拟端点
通知后,marketplace 会调用卖家侧的此端点,以获取当前价格和库存数据。
import { RequestHandler } from "express";
interface SimulationItem {
id: string;
quantity: number;
seller: string;
}
interface SimulationRequest {
items: SimulationItem[];
postalCode?: string;
country?: string;
}
interface SimulationResponseItem {
id: string;
requestIndex: number;
quantity: number;
seller: string;
price: number;
listPrice: number;
sellingPrice: number;
priceValidUntil: string;
availability: string;
merchantName: string;
}
const fulfillmentSimulationHandler: RequestHandler = async (req, res) => {
const { items, postalCode, country }: SimulationRequest = req.body;
const responseItems: SimulationResponseItem[] = await Promise.all(
items.map(async (item, index) => {
// Fetch current price and inventory from your system
const skuInfo = await getSkuFromLocalCatalog(item.id);
return {
id: item.id,
requestIndex: index,
quantity: Math.min(item.quantity, skuInfo.availableQuantity),
seller: item.seller,
price: skuInfo.price,
listPrice: skuInfo.listPrice,
sellingPrice: skuInfo.sellingPrice,
priceValidUntil: new Date(Date.now() + 3600000).toISOString(),
availability: skuInfo.availableQuantity > 0 ? "available" : "unavailable",
merchantName: "sellerAccountName",
};
})
);
// CRITICAL: Must respond within 2.5 seconds or products show as unavailable
res.json({
items: responseItems,
postalCode: postalCode ?? "",
country: country ?? "",
});
};
async function getSkuFromLocalCatalog(skuId: string): Promise<{
price: number;
listPrice: number;
sellingPrice: number;
availableQuantity: number;
}> {
// Replace with your actual catalog/inventory lookup
return {
price: 9990,
listPrice: 12990,
sellingPrice: 9990,
availableQuantity: 15,
};
}
通知价格和库存变更
分别发送价格和库存更新通知。URL 中的 {sellerSkuId} 部分是卖家的 SKU 标识符(即你在目录中以及 changenotification/{sellerId}/{sellerSkuId}/建议中使用的同一代码)。除非你的集成明确以此方式作为键,否则不要在此处传入 marketplace 内部的 VTEX SKU ID。
async function notifyPriceChange(
client: AxiosInstance,
sellerId: string,
sellerSkuId: string
): Promise<void> {
await client.post(
`/notificator/${sellerId}/changenotification/${sellerSkuId}/price`
);
}
async function notifyInventoryChange(
client: AxiosInstance,
sellerId: string,
sellerSkuId: string
): Promise<void> {
await client.post(
`/notificator/${sellerId}/changenotification/${sellerSkuId}/inventory`
);
}
async function syncPriceAndInventory(
client: AxiosInstance,
sellerId: string,
sellerSkuIds: string[]
): Promise<void> {
for (const sellerSkuId of sellerSkuIds) {
await notifyPriceChange(client, sellerId, sellerSkuId);
await notifyInventoryChange(client, sellerId, sellerSkuId);
// Throttle to avoid rate limits
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
完整示例
import axios from "axios";
async function runCatalogSync(): Promise<void> {
const config: SellerConnectorConfig = {
marketplaceAccount: "mymarketplace",
sellerId: "externalseller01",
appKey: process.env.VTEX_APP_KEY!,
appToken: process.env.VTEX_APP_TOKEN!,
};
const client = createMarketplaceClient(config);
// Fetch SKUs that need syncing from your system
const skusToSync = await getLocalSkusNeedingSync();
for (const sku of skusToSync) {
const skuSuggestion: SkuSuggestion = {
ProductName: sku.productName,
SkuName: sku.skuName,
ImageUrl: sku.imageUrl,
ProductDescription: sku.description,
BrandName: sku.brand,
CategoryFullPath: sku.categoryPath,
EAN: sku.ean,
Height: sku.height,
Width: sku.width,
Length: sku.length,
WeightKg: sku.weight,
SkuSpecifications: sku.specifications,
};
const result = await notifyAndSync(
client,
config.marketplaceAccount,
config.sellerId,
sku.sellerSkuId,
skuSuggestion
);
console.log(`SKU ${sku.sellerSkuId}: ${result.action}`);
// Throttle between SKU operations
await new Promise((resolve) => setTimeout(resolve, 200));
}
// Sync prices and inventory for all active SKUs (seller SKU IDs)
const activeSellerSkuIds = skusToSync.map((s) => s.sellerSkuId);
await syncPriceAndInventory(client, config.sellerId, activeSellerSkuIds);
}
async function getLocalSkusNeedingSync(): Promise<
Array<{
sellerSkuId: string;
productName: string;
skuName: string;
imageUrl: string;
description: string;
brand: string;
categoryPath: string;
ean: string;
height: number;
width: number;
length: number;
weight: number;
specifications: Array<{ FieldName: string; FieldValues: string[] }>;
}>
> {
// Replace with your actual data source
return [];
}
常见故障模式
-
使用卖家的 SKU ID 调用单段变更通知。
POST .../changenotification/{skuId}会解析市场 SKU ID。传入卖家的目录代码将无法匹配目标 SKU,并导致集成流程中断。卖家连接器必须使用POST .../changenotification/{sellerId}/{sellerSkuId}。如果公开 API 参考文档在正文中描述了sellerId,但 URL 中只有一段路径,则应视为文档不一致,并遵循上述路径格式。 -
在紧密循环中轮询建议状态。 建议审批是一个由市场人工或半自动处理的流程,可能需要几分钟到几天。频繁轮询会浪费 API 配额,还可能触发速率限制,导致整个集成被阻止。请使用定时任务(cron)定期检查建议状态(例如每 15-30 分钟),或实现基于 webhook 的通知系统。
-
忽略履约模拟超时。 卖家的履约模拟端点会执行复杂的数据库查询或外部 API 调用,导致响应时间超出限制。VTEX 市场最多等待 2.5 秒 获取履约模拟响应。超时后,商品会被视为不可售/未激活,且不会显示在店面或结账页面中。使用内存缓存或 Redis 缓存预先缓存价格和库存数据,并通过事件驱动的更新机制确保模拟端点立即响应。
import { RequestHandler } from "express";
// Correct: Cache-first approach for fast fulfillment simulation
const cachedPriceInventory = new Map<string, {
price: number;
listPrice: number;
sellingPrice: number;
availableQuantity: number;
updatedAt: number;
}>();
const fastFulfillmentSimulation: RequestHandler = async (req, res) => {
const { items } = req.body;
const responseItems = items.map((item: SimulationItem, index: number) => {
const cached = cachedPriceInventory.get(item.id);
if (!cached) {
return {
id: item.id,
requestIndex: index,
quantity: 0,
availability: "unavailable",
};
}
return {
id: item.id,
requestIndex: index,
quantity: Math.min(item.quantity, cached.availableQuantity),
price: cached.price,
listPrice: cached.listPrice,
sellingPrice: cached.sellingPrice,
availability: cached.availableQuantity > 0 ? "available" : "unavailable",
};
});
// Responds in < 50ms from cache
res.json({ items: responseItems });
};
审核清单
- 是否使用了变更通知 + SKU 建议流程(而非直接写入 Catalog API)?
- 目录变更通知是否使用
.../changenotification/{sellerId}/{sellerSkuId}(而非使用卖家 SKU 代码调用单段市场 SKU 路由)? - 集成是否处理了 changenotification 返回的 200(已存在)和 404(新建)响应?
- SKU 建议更新是否受状态检查保护(仅在状态为“待处理”时更新)?
-
PUT/GET建议请求是否发送到https://api.vtex.com/{account}/suggestions/{sellerId}/{sellerSkuId},而不是店铺主机名? - 批量目录通知是否经过限流,并在遇到 429 时采用指数退避?
- 履约模拟端点是否能在 2.5 秒内响应?
- 价格和库存通知是否通过正确的
/notificator/{sellerId}/changenotification/{sellerSkuId}/price|inventory路径发送(路径中使用卖家 SKU ID)? - 占位值(账户名称、卖家 ID、API 密钥)是否已替换为实际值?
参考
- 外部卖家连接器指南 — 外部卖家接入 VTEX 市场的完整集成流程
- 变更通知(市场 SKU ID) —
POST .../changenotification/{skuId};路径仅使用市场 SKU ID - 变更通知(卖家 ID 和卖家 SKU ID) —
POST .../changenotification/{sellerId}/{skuId};卖家连接器目录集成请使用此接口 - 发送 SKU 建议(
PUT /suggestions/{sellerId}/{sellerSkuId}) — 基础 URL 为https://api.vtex.com/{accountName} - 市场 API — 建议 — 完整的建议 API 参考
- 市场 API - 管理建议(指南) — SKU 建议工作流的详细指南
- VTEX 市场目录管理 — 市场侧目录操作和 SKU 审批工作流