marketplace-catalog-sync
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCatalog & SKU Integration
目录与SKU集成
When this skill applies
适用场景
Use this skill when building a seller connector that needs to push product catalog data into a VTEX marketplace, handle SKU approval workflows, or keep prices and inventory synchronized.
- Building the Change Notification flow to register and update SKUs
- Implementing the SKU suggestion lifecycle (send → pending → approved/denied)
- Mapping product data to the VTEX catalog schema
- Synchronizing prices and inventory via notification endpoints
Do not use this skill for:
- Marketplace-side catalog operations (direct Catalog API writes)
- Order fulfillment or invoice handling (see )
marketplace-fulfillment - Rate limiting patterns in isolation (see )
marketplace-rate-limiting
当你需要构建卖家连接器,将商品目录数据推送至VTEX市场、处理SKU审批工作流,或保持价格与库存同步时,可使用本指南。
- 构建变更通知流程来注册和更新SKU
- 实现SKU建议生命周期(提交→待审核→通过/驳回)
- 将产品数据映射到VTEX目录Schema
- 通过通知端点同步价格与库存
请勿将本指南用于以下场景:
- 市场侧目录操作(直接调用Catalog API写入数据)
- 订单履约或发票处理(参考)
marketplace-fulfillment - 单独的限流模式实现(参考)
marketplace-rate-limiting
Decision rules
决策规则
VTEX exposes two routes under . They are not interchangeable — the path shape tells the platform which identifier you are sending.
POSTapi/catalog_system/pvt/skuseller/changenotification| Route | Path pattern | Meaning |
|---|---|---|
| Change notification with marketplace SKU ID | | |
| Change notification with seller ID and seller SKU ID | | |
Seller connector integrations MUST use the second route. Official docs sometimes mix descriptions between these two — trust the URL shape: the seller-scoped flow always uses two path segments after .
changenotification/SKU suggestions (/) must go to , not the store hostname. The same App Key and App Token apply. Do not build suggestion URLs using — that is a different Catalog System surface.
PUTGEThttps://api.vtex.com/{accountName}/suggestions/{sellerId}/{sellerSkuId}{account}.vtexcommercestable.com.br/api/catalog_system/pvt/sku/seller/...- For seller-side catalog integration, use (seller Id in the marketplace account + seller’s SKU ID). A 200 OK means the SKU already exists in the marketplace for that seller (update path); a 404 Not Found means it does not (send a SKU suggestion). Do not use
POST /api/catalog_system/pvt/skuseller/changenotification/{sellerId}/{sellerSkuId}with the seller’s SKU code — that single-segment route expects the marketplace SKU ID.POST .../changenotification/{skuId} - Use only when the identifier you have is the VTEX marketplace SKU ID (no seller segment in the path).
POST .../changenotification/{skuId} - Use on
PUTunder/suggestions/{sellerId}/{sellerSkuId}(Send SKU Suggestion) to register or update pending suggestions. The seller does not own the catalog — every new SKU must go through the suggestion/approval workflow.https://api.vtex.com/{accountName} - Use separate notification endpoints for price and inventory: and
POST /notificator/{sellerId}/changenotification/{sellerSkuId}/price. The path segment afterPOST /notificator/{sellerId}/changenotification/{sellerSkuId}/inventoryis the seller SKU ID (the seller’s own SKU code — the same identifier used in suggestions and seller-scoped catalog flows), not the marketplace VTEX SKU ID. Reference docs may label this segmentchangenotification/; read it as sellerSkuId in seller-connector integrations.skuId - After price/inventory notifications, the marketplace calls the seller's Fulfillment Simulation endpoint (). This endpoint must respond within 2.5 seconds or the product is considered unavailable.
POST /pvt/orderForms/simulation - Suggestions can only be updated while in "pending" state. Once approved or denied, the seller cannot modify them.
Architecture/Data Flow:
text
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 ────▶│VTEX在下暴露了两个路由。二者不可互换——路径结构会告知平台你发送的是哪种标识符。
api/catalog_system/pvt/skuseller/changenotificationPOST| 路由 | 路径结构 | 含义 |
|---|---|---|
| 携带市场SKU ID的变更通知 | | |
| 携带卖家ID和卖家SKU ID的变更通知 | | |
卖家连接器集成必须使用第二个路由。官方文档有时会混淆这两个路由的描述,请以URL结构为准:卖家维度的流程在后总会带有两个路径段。
changenotification/SKU建议(/)必须发送到,而非店铺域名。使用的App Key和App Token保持一致。请勿使用构建建议URL——这是另一个Catalog System接口。
PUTGEThttps://api.vtex.com/{accountName}/suggestions/{sellerId}/{sellerSkuId}{account}.vtexcommercestable.com.br/api/catalog_system/pvt/sku/seller/...- 对于卖家侧目录集成,使用(市场账户中的卖家ID + 卖家侧SKU ID)。返回200 OK表示该卖家的SKU已在市场中存在(走更新流程);返回404 Not Found表示SKU不存在(需要提交SKU建议)。不要将卖家侧SKU编码传入
POST /api/catalog_system/pvt/skuseller/changenotification/{sellerId}/{sellerSkuId}使用——该单段路径要求传入市场侧的SKU ID。POST .../changenotification/{skuId} - 仅当你持有的标识符是VTEX市场侧的SKU ID时,才使用(路径中没有卖家段)。
POST .../changenotification/{skuId} - 在**域名下对
https://api.vtex.com/{accountName}使用/suggestions/{sellerId}/{sellerSkuId}**方法(提交SKU建议)来注册或更新待审核的建议。卖家不拥有目录所有权——所有新SKU都必须经过建议/审批工作流。PUT - 价格和库存需使用单独的通知端点:和
POST /notificator/{sellerId}/changenotification/{sellerSkuId}/price。POST /notificator/{sellerId}/changenotification/{sellerSkuId}/inventory后的路径段是卖家侧SKU ID(卖家自身的SKU编码——和建议、卖家维度目录流程中使用的标识符一致),而非市场侧VTEX SKU ID。参考文档可能会将该段标记为changenotification/,在卖家连接器集成中请将其视为sellerSkuId。skuId - 价格/库存通知发送后,市场会调用卖家的履约模拟端点()。该端点必须在2.5秒内响应,否则商品会被标记为不可用。
POST /pvt/orderForms/simulation - 建议仅在“待审核”状态下可更新。一旦通过或驳回,卖家无法再修改。
架构/数据流:
text
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 ────▶│Hard constraints
硬性约束
Constraint: Use SKU Integration API, Not Direct Catalog API
约束:使用SKU集成API,而非直接调用Catalog API
External sellers MUST use the Change Notification + SKU Suggestion flow to integrate SKUs. Direct Catalog API writes ( or ) are for marketplace-side operations only.
POST /api/catalog/pvt/productPOST /api/catalog/pvt/stockkeepingunitWhy this matters
The seller does not own the catalog. Direct catalog writes will fail with 403 Forbidden or create orphaned entries that bypass the approval workflow. The suggestion mechanism ensures marketplace quality control.
Detection
If you see direct Catalog API calls for product/SKU creation (e.g., , ) from a seller integration → warn that the SKU Integration API should be used instead.
POST /api/catalog/pvt/productPOST /api/catalog/pvt/stockkeepingunitCorrect
typescript
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
typescript
// 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
}外部卖家必须使用变更通知+SKU建议流程来集成SKU。直接调用Catalog API写入(或)仅适用于市场侧操作。
POST /api/catalog/pvt/productPOST /api/catalog/pvt/stockkeepingunit重要性说明
卖家不拥有目录所有权。直接写入Catalog API会返回403 Forbidden错误,或创建绕过审批流程的孤立条目。建议机制可保障市场的内容质量管控。
问题检测
如果在卖家集成中发现直接调用Catalog API创建产品/SKU(例如、)→ 提示应当使用SKU集成API。
POST /api/catalog/pvt/productPOST /api/catalog/pvt/stockkeepingunit正确示例
typescript
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;
}
}
}错误示例
typescript
// 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
}Constraint: Handle Rate Limiting on Catalog Notifications
约束:处理目录通知的限流规则
All catalog notification calls MUST implement 429 handling with exponential backoff. Batch notifications MUST be throttled to respect VTEX API rate limits.
Why this matters
The Change Notification endpoint is rate-limited. Sending bulk notifications without throttling will trigger 429 responses and temporarily block the seller's API access, stalling the entire integration.
Detection
If you see catalog notification calls without 429 handling or retry logic → STOP and add rate limiting. If you see a tight loop sending notifications without delays → warn about rate limiting.
Correct
typescript
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
typescript
// 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}`
)
)
);
}所有目录通知调用必须实现429状态码的处理逻辑,搭配指数退避策略。批量通知必须进行限流,以遵守VTEX API的速率限制。
重要性说明
变更通知端点有速率限制。未加限流的批量通知会触发429响应,临时封禁卖家的API访问权限,导致整个集成停滞。
问题检测
如果发现目录通知调用没有429处理或重试逻辑→ 立即停止并添加限流逻辑。如果发现紧循环发送通知没有延迟→ 提示限流风险。
正确示例
typescript
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));
}
}
}错误示例
typescript
// 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}`
)
)
);
}Constraint: Handle Suggestion Lifecycle States
约束:处理建议生命周期状态
Sellers MUST check the suggestion state before attempting updates. Suggestions can only be updated while in pending state.
Why this matters
Attempting to update an already-approved or denied suggestion will fail silently or create duplicate entries. An approved suggestion becomes an SKU owned by the marketplace.
Detection
If you see SKU suggestion updates without checking current suggestion status → warn about suggestion lifecycle handling.
Correct
typescript
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
typescript
// 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
);
}卖家在尝试更新建议前必须检查建议状态。仅待审核状态的建议可更新。
重要性说明
尝试更新已通过或已驳回的建议会静默失败,或创建重复条目。已通过的建议会成为市场所有的SKU。
问题检测
如果发现SKU建议更新前未检查当前建议状态→ 提示需要处理建议生命周期。
正确示例
typescript
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;
}
}错误示例
typescript
// 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
);
}Preferred pattern
推荐模式
Set Up the Seller Connector Client
配置卖家连接器客户端
Create an authenticated HTTP client for communicating with the VTEX marketplace.
typescript
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).创建认证的HTTP客户端,用于和VTEX市场通信。
typescript
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).Implement the Change Notification Flow
实现变更通知流程
Handle both the "exists" (200) and "new" (404) scenarios from the changenotification endpoint.
typescript
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 };
}
}处理变更通知端点返回的“已存在”(200)和“新增”(404)两种场景。
typescript
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 };
}
}Implement the Fulfillment Simulation Endpoint
实现履约模拟端点
The marketplace calls this endpoint on the seller's side to retrieve current price and inventory data after a notification.
typescript
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,
};
}市场会在通知发送后调用卖家侧的该端点,拉取最新的价格和库存数据。
typescript
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,
};
}Notify Price and Inventory Changes
发送价格和库存变更通知
Send separate notifications for price and inventory updates. The segment in the URL is the seller’s SKU identifier (same code you use in your catalog and in / suggestions). Do not pass the marketplace’s internal VTEX SKU ID here unless your integration is explicitly keyed that way.
{sellerSkuId}changenotification/{sellerId}/{sellerSkuId}typescript
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));
}
}价格和库存更新需发送单独的通知。URL中的段是卖家侧SKU标识符(和你的目录、、建议流程中使用的编码一致)。除非你的集成明确适配,否则不要传入市场内部的VTEX SKU ID。
{sellerSkuId}changenotification/{sellerId}/{sellerSkuId}typescript
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));
}
}Complete Example
完整示例
typescript
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 [];
}typescript
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 [];
}Common failure modes
常见故障模式
-
Calling the single-segment change notification with the seller’s SKU ID.resolves the marketplace SKU ID. Passing the seller’s catalog code there will not match the intended SKU and breaks the integration flow. Seller connectors must use
POST .../changenotification/{skuId}. If public API reference text describesPOST .../changenotification/{sellerId}/{sellerSkuId}in the body but shows a one-segment URL, treat that as a documentation mismatch and follow the path shape above.sellerId -
Polling for suggestion status in tight loops. Suggestion approval is a manual or semi-automatic marketplace process that can take minutes to days. Tight polling wastes API quota and may trigger rate limits that block the entire integration. Use a scheduled job (cron) to check suggestion statuses periodically (e.g., every 15-30 minutes), or implement a webhook-based notification system.
-
Ignoring the fulfillment simulation timeout. The seller's fulfillment simulation endpoint performs complex database queries or external API calls that exceed the response time limit. VTEX marketplaces wait a maximum of 2.5 seconds for a fulfillment simulation response. After that, the product is considered unavailable/inactive and won't appear in the storefront or checkout. Pre-cache price and inventory data using in-memory or Redis cache with event-driven updates so the simulation endpoint responds instantly.
typescript
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 ID传入单段路径的变更通知接口。解析的是市场侧SKU ID。传入卖家侧目录编码无法匹配目标SKU,会破坏集成流程。卖家连接器必须使用
POST .../changenotification/{skuId}。如果公开API参考文档描述了请求体中的POST .../changenotification/{sellerId}/{sellerSkuId},但示例URL是单段路径,视为文档不匹配,遵循上述路径结构即可。sellerId - 紧循环轮询建议状态。建议审批是市场的手动或半自动流程,可能需要数分钟到数天时间。高频轮询会浪费API配额,可能触发速率限制封禁整个集成。使用定时任务(cron)定期检查建议状态(例如每15-30分钟一次),或实现基于Webhook的通知系统。
- 忽略履约模拟的超时限制。卖家的履约模拟端点如果执行复杂的数据库查询或外部API调用,会超出响应时间限制。VTEX市场最多等待2.5秒获取履约模拟响应。超时后商品会被标记为不可用/未激活,不会出现在店铺前台或结算页面。使用内存或Redis缓存预存价格和库存数据,搭配事件驱动更新,确保模拟端点可以即时响应。
typescript
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 });
};Review checklist
审核检查清单
- Is the Change Notification + SKU Suggestion flow used (not direct Catalog API writes)?
- Does catalog change notification use (not the single-segment marketplace-SKU route with a seller SKU code)?
.../changenotification/{sellerId}/{sellerSkuId} - Does the integration handle both 200 (exists) and 404 (new) responses from changenotification?
- Are SKU suggestion updates guarded by a status check (only update while "Pending")?
- Are /
PUTsuggestion calls sent toGET, not to the store hostname?https://api.vtex.com/{account}/suggestions/{sellerId}/{sellerSkuId} - Are batch catalog notifications throttled with 429 handling and exponential backoff?
- Does the fulfillment simulation endpoint respond within 2.5 seconds?
- Are price and inventory notifications sent via the correct paths (seller SKU ID in the path)?
/notificator/{sellerId}/changenotification/{sellerSkuId}/price|inventory - Are placeholder values (account names, seller IDs, API keys) replaced with real values?
- 是否使用了变更通知+SKU建议流程(而非直接调用Catalog API写入)?
- 目录变更通知是否使用(而非传入卖家SKU编码调用单段的市场SKU路由)?
.../changenotification/{sellerId}/{sellerSkuId} - 集成是否处理了变更通知返回的200(已存在)和404(新增)两种响应?
- SKU建议更新是否有状态检查(仅“待审核”状态可更新)?
- /
PUT建议请求是否发送到GET,而非店铺域名?https://api.vtex.com/{account}/suggestions/{sellerId}/{sellerSkuId} - 批量目录通知是否做了限流,搭配429处理和指数退避?
- 履约模拟端点是否能在2.5秒内响应?
- 价格和库存通知是否通过正确的路径发送(路径中为卖家SKU ID)?
/notificator/{sellerId}/changenotification/{sellerSkuId}/price|inventory - 占位符值(账户名、卖家ID、API密钥)是否已替换为真实值?
Reference
参考资料
- External Seller Connector Guide — Complete integration flow for external sellers connecting to VTEX marketplaces
- Change notification (marketplace SKU ID) — ; path uses marketplace SKU ID only
POST .../changenotification/{skuId} - Change notification (seller ID and seller SKU ID) — ; use this for seller connector catalog integration
POST .../changenotification/{sellerId}/{skuId} - Send SKU Suggestion () — base URL
PUT /suggestions/{sellerId}/{sellerSkuId}https://api.vtex.com/{accountName} - Marketplace API — Suggestions — full Suggestions API reference
- Marketplace API - Manage Suggestions (guide) — narrative guide for SKU suggestions workflow
- Catalog Management for VTEX Marketplace — Marketplace-side catalog operations and SKU approval workflows
- 外部卖家连接器指南 — 外部卖家接入VTEX市场的完整集成流程
- 变更通知(市场SKU ID) — ;路径仅使用市场侧SKU ID
POST .../changenotification/{skuId} - 变更通知(卖家ID和卖家SKU ID) — ;卖家连接器目录集成请使用该路由
POST .../changenotification/{sellerId}/{skuId} - 提交SKU建议() — 基础URL为
PUT /suggestions/{sellerId}/{sellerSkuId}https://api.vtex.com/{accountName} - 市场API — 建议 — 完整的建议API参考
- 市场API — 管理建议(指南) — SKU建议工作流的说明指南
- VTEX市场目录管理 — 市场侧目录操作和SKU审批工作流