marketplace-catalog-sync

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Catalog & 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 marketplace、处理SKU审批工作流,或保持价格与库存同步时,可使用本技能。
  • 构建变更通知流以注册和更新SKU
  • 实现SKU建议生命周期(提交→待处理→批准/拒绝)
  • 将产品数据映射至VTEX目录架构
  • 通过通知端点同步价格与库存
以下场景请勿使用本技能:
  • 平台端目录操作(直接调用Catalog API写入)
  • 订单履约或发票处理(参考
    marketplace-fulfillment
  • 单独的限流模式(参考
    marketplace-rate-limiting

Decision rules

决策规则

VTEX exposes two
POST
routes under
api/catalog_system/pvt/skuseller/changenotification
. They are not interchangeable — the path shape tells the platform which identifier you are sending.
RoutePath patternMeaning
Change notification with marketplace SKU ID
.../changenotification/{skuId}
{skuId}
is the SKU ID in the marketplace catalog (VTEX). There is no
sellerId
in the URL.
Change notification with seller ID and seller SKU ID
.../changenotification/{sellerId}/{skuId}
{sellerId}
is the seller account on the marketplace;
{skuId}
is the seller's own SKU code (the same ID used in
PUT
SKU Suggestion paths).
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 (
PUT
/
GET
) must go to
https://api.vtex.com/{accountName}/suggestions/{sellerId}/{sellerSkuId}
, not the store hostname. The same App Key and App Token apply. Do not build suggestion URLs using
{account}.vtexcommercestable.com.br/api/catalog_system/pvt/sku/seller/...
— that is a different Catalog System surface.
  • For seller-side catalog integration, use
    POST /api/catalog_system/pvt/skuseller/changenotification/{sellerId}/{sellerSkuId}
    (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 .../changenotification/{skuId}
    with the seller’s SKU code — that single-segment route expects the marketplace SKU ID.
  • Use
    POST .../changenotification/{skuId}
    only when the identifier you have is the VTEX marketplace SKU ID (no seller segment in the path).
  • Use
    PUT
    on
    /suggestions/{sellerId}/{sellerSkuId}
    under
    https://api.vtex.com/{accountName}
    (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.
  • Use separate notification endpoints for price and inventory:
    POST /notificator/{sellerId}/changenotification/{sellerSkuId}/price
    and
    POST /notificator/{sellerId}/changenotification/{sellerSkuId}/inventory
    . The path segment after
    changenotification/
    is 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 segment
    skuId
    ; read it as sellerSkuId in seller-connector integrations.
  • After price/inventory notifications, the marketplace calls the seller's Fulfillment Simulation endpoint (
    POST /pvt/orderForms/simulation
    ). This endpoint must respond within 2.5 seconds or the product is considered unavailable.
  • 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/changenotification
下提供了两个
POST
路由,二者不可互换——路径结构决定了你要传入的标识符类型。
路由路径模式含义
带平台SKU ID的变更通知
.../changenotification/{skuId}
{skuId}
VTEX平台目录中的SKU ID。URL中不包含
sellerId
带卖家ID和卖家SKU ID的变更通知
.../changenotification/{sellerId}/{skuId}
{sellerId}
是卖家在平台的账户名;
{skuId}
卖家自身的SKU编码(与
PUT
SKU建议路径中使用的ID一致)。
卖家连接器集成必须使用第二个路由。官方文档有时会混淆二者的描述——请以URL结构为准:卖家专属流程的
changenotification/
后总是包含两个路径段。
SKU建议(
PUT
/
GET
)必须发送至
https://api.vtex.com/{accountName}/suggestions/{sellerId}/{sellerSkuId}
,而非店铺域名。使用相同的App Key和App Token。请勿使用
{account}.vtexcommercestable.com.br/api/catalog_system/pvt/sku/seller/...
构建建议URL——这属于另一个Catalog System接口。
  • 对于卖家端目录集成,使用
    POST /api/catalog_system/pvt/skuseller/changenotification/{sellerId}/{sellerSkuId}
    (平台中的卖家ID + 卖家自身的SKU ID)。返回200 OK表示该SKU已存在于平台中(执行更新流程);返回404 Not Found表示SKU不存在(需提交SKU建议)。请勿使用
    POST .../changenotification/{skuId}
    并传入卖家的SKU编码——该单段路由要求传入平台的SKU ID。
  • 仅当你持有的标识符是VTEX平台SKU ID时,才使用
    POST .../changenotification/{skuId}
    (路径中无卖家段)。
  • 使用**
    PUT
    请求
    /suggestions/{sellerId}/{sellerSkuId}
    ,请求地址为
    https://api.vtex.com/{accountName}
    **(提交SKU建议),以注册或更新待处理的建议。卖家不拥有目录权限——所有新SKU必须经过建议/审批流程。
  • 为价格和库存使用单独的通知端点:
    POST /notificator/{sellerId}/changenotification/{sellerSkuId}/price
    POST /notificator/{sellerId}/changenotification/{sellerSkuId}/inventory
    changenotification/
    后的路径段是卖家SKU ID(卖家自身的SKU编码——与建议和卖家专属目录流程中使用的标识符一致),而非平台的VTEX SKU ID。参考文档可能将该段标记为
    skuId
    ;在卖家连接器集成中,请将其理解为sellerSkuId
  • 发送价格/库存通知后,平台会调用卖家的履约模拟端点(
    POST /pvt/orderForms/simulation
    )。该端点必须在2.5秒内响应,否则产品会被标记为不可用。
  • 仅当建议处于“待处理”状态时,才可对其进行更新。一旦建议被批准或拒绝,卖家无法再修改。
架构/数据流:
text
卖家                          VTEX Marketplace
  │                                    │
  │─── POST changenotification ──────▶│
  │◀── 200(已存在)或404(新增) ────│
  │                                    │
  │─── PUT 提交SKU建议 ──────▶│  (若返回404)
  │                                    │── 进入“已接收SKU”待处理状态
  │                                    │── 平台批准/拒绝建议
  │                                    │
  │─── POST 价格通知 ──────▶│
  │◀── POST 履约模拟 ───│  (平台获取数据)
  │─── 返回价格/库存数据 ────▶│

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 (
POST /api/catalog/pvt/product
or
POST /api/catalog/pvt/stockkeepingunit
) are for marketplace-side operations only.
Why 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.,
POST /api/catalog/pvt/product
,
POST /api/catalog/pvt/stockkeepingunit
) from a seller integration → warn that the SKU Integration API should be used instead.
Correct
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/product
POST /api/catalog/pvt/stockkeepingunit
)仅适用于平台端操作。
原因
卖家不拥有目录权限。直接写入目录会返回403 Forbidden错误,或创建绕过审批流程的孤立条目。建议机制可确保平台的质量控制。
检测方式
若发现卖家集成中存在直接调用Catalog API创建产品/SKU的情况(如
POST /api/catalog/pvt/product
POST /api/catalog/pvt/stockkeepingunit
),需提醒应使用SKU集成API。
正确示例
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}`;

  // 步骤1:卖家专属变更通知(Catalog API — 店铺域名)
  try {
    await client.post(
      `${storeBaseUrl}/api/catalog_system/pvt/skuseller/changenotification/${sellerId}/${sellerSkuId}`
    );
    // 200 OK — SKU已存在,平台将通过履约模拟获取更新
    console.log(`SKU ${sellerSkuId}已存在于平台,触发更新`);
  } catch (error: unknown) {
    if (axios.isAxiosError(error) && error.response?.status === 404) {
      // 404 — SKU不存在,提交建议
      console.log(`未找到SKU ${sellerSkuId},正在提交建议`);
      await client.put(suggestionUrl, skuData);
      console.log(`已为${sellerSkuId}提交SKU建议`);
    } else {
      throw error;
    }
  }
}
错误示例
typescript
// 错误:使用仅适用于平台SKU的路径传入卖家SKU编码(路由错误)。
// .../changenotification/{skuId}要求传入VTEX平台SKU ID,而非sellerSkuId。
await client.post(
  `https://${marketplaceAccount}.vtexcommercestable.com.br/api/catalog_system/pvt/skuseller/changenotification/${sellerSkuId}`
);

// 错误:在店铺域名+Catalog路径下提交SKU建议——公开约定是使用api.vtex.com + /suggestions/...
await client.put(
  `https://${marketplaceAccount}.vtexcommercestable.com.br/api/catalog_system/pvt/sku/seller/${sellerId}/suggestion/${sellerSkuId}`,
  skuData
);

// 错误:卖家直接写入平台目录——绕过建议/审批流程;将返回403错误
async function createSkuDirectly(
  client: AxiosInstance,
  marketplaceAccount: string,
  productData: Record<string, unknown>
): Promise<void> {
  // 直接写入目录——卖家无此权限
  await client.post(
    `https://${marketplaceAccount}.vtexcommercestable.com.br/api/catalog/pvt/product`,
    productData
  );
  // 将失败:403 Forbidden — 卖家无目录写入权限
  // 将失败:403 Forbidden — 卖家无目录写入权限
}

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(`触发限流。等待${retryAfter}秒后重试。`);
            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);
      }
    }

    // 批次间限流
    if (i + concurrency < sellerSkuIds.length) {
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }
}
错误示例
typescript
// 错误:无限流、无错误处理,循环密集发送请求
async function notifyAllSkus(
  client: AxiosInstance,
  baseUrl: string,
  sellerId: string,
  sellerSkuIds: string[]
): Promise<void> {
  // 同时发送所有请求——将触发429限流
  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}`;

  // 更新前检查当前建议状态
  try {
    const response = await client.get(suggestionUrl);

    const suggestion = response.data;
    if (suggestion.Status === "Pending") {
      // 可安全更新——建议尚未处理
      await client.put(suggestionUrl, updatedData);
      return true;
    }

    // 已批准或拒绝——无法更新
    console.warn(
      `SKU ${sellerSkuId}的建议状态为${suggestion.Status},无法更新。 ` +
        `请使用changenotification更新已存在的SKU。`
    );
    return false;
  } catch {
    // 建议可能不存在——作为新建议提交
    await client.put(suggestionUrl, updatedData);
    return true;
  }
}
错误示例
typescript
// 错误:不检查状态,直接发送建议更新
async function blindUpdateSuggestion(
  client: AxiosInstance,
  marketplaceAccount: string,
  sellerId: string,
  sellerSkuId: string,
  data: Record<string, unknown>
): Promise<void> {
  // 若建议已批准,此操作会静默失败
  // 或创建重复条目,导致平台操作员混淆
  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).
创建用于与VTEX marketplace通信的认证HTTP客户端。
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路由(如changenotification)使用店铺域名。
    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,
  });
}

// 对于https://api.vtex.com/{account}/suggestions/...上的PUT/GET请求,使用相同的请求头
// (在同一个axios实例上传入完整URL,或为仅处理建议的调用将baseURL设置为https://api.vtex.com/{account})。

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 };
  }
}
处理changenotification端点返回的“已存在”(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已存在——平台将调用履约模拟获取更新
    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 : "未知错误";
        return { skuId: sellerSkuId, action: "error", error: message };
      }
    }

    const message = error instanceof Error ? error.message : "未知错误";
    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) => {
      // 从你的系统中获取当前价格和库存
      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",
      };
    })
  );

  // 关键:必须在2.5秒内响应,否则产品会被标记为不可用
  res.json({
    items: responseItems,
    postalCode: postalCode ?? "",
    country: country ?? "",
  });
};

async function getSkuFromLocalCatalog(skuId: string): Promise<{
  price: number;
  listPrice: number;
  sellingPrice: number;
  availableQuantity: number;
}> {
  // 替换为你的实际目录/库存查询逻辑
  return {
    price: 9990,
    listPrice: 12990,
    sellingPrice: 9990,
    availableQuantity: 15,
  };
}

Notify Price and Inventory Changes

通知价格与库存变更

Send separate notifications for price and inventory updates. The
{sellerSkuId}
segment in the URL is the seller’s SKU identifier (same code you use in your catalog and in
changenotification/{sellerId}/{sellerSkuId}
/ suggestions). Do not pass the marketplace’s internal VTEX SKU ID here unless your integration is explicitly keyed that way.
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中的
{sellerSkuId}
段是卖家的SKU标识符(与你的目录、
changenotification/{sellerId}/{sellerSkuId}
/建议中使用的编码一致)。除非你的集成明确使用平台的内部VTEX SKU ID,否则请勿在此处传入该ID。
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);

    // 限流以避免触发速率限制
    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);

  // 从你的系统中获取需要同步的SKU
  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}`);

    // SKU操作间限流
    await new Promise((resolve) => setTimeout(resolve, 200));
  }

  // 同步所有活跃SKU的价格和库存(卖家SKU ID)
  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[] }>;
  }>
> {
  // 替换为你的实际数据源
  return [];
}

Common failure modes

常见失败模式

  • Calling the single-segment change notification with the seller’s SKU ID.
    POST .../changenotification/{skuId}
    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/{sellerId}/{sellerSkuId}
    . If public API reference text describes
    sellerId
    in the body but shows a one-segment URL, treat that as a documentation mismatch and follow the path shape above.
  • 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
    POST .../changenotification/{skuId}
    解析的是平台SKU ID。传入卖家的目录编码将无法匹配目标SKU,导致集成流程中断。卖家连接器必须使用
    POST .../changenotification/{sellerId}/{sellerSkuId}
    。若公开API参考文本描述中提到请求体包含
    sellerId
    ,但显示的是单段URL,请将其视为文档错误,并遵循上述路径结构。
  • 密集循环轮询建议状态。建议审批是平台的手动或半自动流程,可能需要数分钟至数天。密集轮询会浪费API配额,可能触发限流导致整个集成被封禁。请使用定时任务(如Cron)定期检查建议状态(例如每15-30分钟一次),或实现基于Webhook的通知系统。
  • 忽略履约模拟超时。卖家的履约模拟端点执行复杂的数据库查询或外部API调用,超出响应时间限制。VTEX平台最多等待2.5秒获取履约模拟响应。超时后,产品会被标记为不可用/ inactive,不会出现在店铺前端或结账流程中。请使用内存缓存或Redis缓存预存价格和库存数据,并通过事件驱动更新,确保模拟端点可即时响应。
typescript
import { RequestHandler } from "express";

// 正确:优先使用缓存实现快速履约模拟
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",
    };
  });

  // 从缓存响应耗时< 50ms
  res.json({ items: responseItems });
};

Review checklist

检查清单

  • Is the Change Notification + SKU Suggestion flow used (not direct Catalog API writes)?
  • Does catalog change notification use
    .../changenotification/{sellerId}/{sellerSkuId}
    (not the single-segment marketplace-SKU route with a seller SKU code)?
  • 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
    PUT
    /
    GET
    suggestion calls sent to
    https://api.vtex.com/{account}/suggestions/{sellerId}/{sellerSkuId}
    , not to the store hostname?
  • 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
    /notificator/{sellerId}/changenotification/{sellerSkuId}/price|inventory
    paths (seller SKU ID in the path)?
  • Are placeholder values (account names, seller IDs, API keys) replaced with real values?
  • 是否使用了变更通知+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密钥)是否已替换为真实值?

Reference

参考资料