marketplace-fulfillment

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Fulfillment, simulation, orders & OMS follow-up

履约、模拟、订单与OMS跟进

When this skill applies

本技能适用场景

Use this skill when building an External Seller integration: VTEX forwards availability, shipping, checkout simulation, and order placement to your fulfillment base URL, and you call the marketplace OMS APIs for invoice and tracking after dispatch.
  • Implementing
    POST /pvt/orderForms/simulation
    (indexation and/or checkout — with or without customer context)
  • Implementing
    POST /pvt/orders
    (order placement — create reservation; return
    orderId
    = reservation id in your system)
  • Handling
    POST /pvt/orders/{sellerOrderId}/fulfill
    (order dispatch after approval — path uses the same id you returned as
    orderId
    at placement)
  • Sending invoice notifications via
    POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice
    (marketplace order id in the path)
  • Updating tracking via
    PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}
  • Implementing partial invoicing for split shipments
Do not use this skill for:
  • Catalog or SKU synchronization (see
    marketplace-catalog-sync
    )
  • Order event consumption via Feed/Hook (see
    marketplace-order-hook
    )
  • General API rate limiting (see
    marketplace-rate-limiting
    )
当你构建外部卖家集成时使用本技能:VTEX将库存可用性、物流配送、结账模拟和下单请求转发到你的履约基础URL,你则需要在发货后调用市场OMSAPI提交发票和物流跟踪信息。
  • 实现
    POST /pvt/orderForms/simulation
    接口(索引构建和/或结账场景——带或不带客户上下文)
  • 实现
    POST /pvt/orders
    接口(下单场景——创建预留记录;返回的**
    orderId
    **为你方系统的预留ID)
  • 处理
    POST /pvt/orders/{sellerOrderId}/fulfill
    请求(订单审核通过后发货——路径使用的ID与你下单时返回的
    orderId
    完全一致)
  • 通过
    POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice
    发送发票通知(路径参数为市场订单ID)
  • 通过
    PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}
    更新物流跟踪信息
  • 实现拆分发货的部分开票功能
以下场景请勿使用本技能:
  • 商品目录或SKU同步(请参考
    marketplace-catalog-sync
  • 通过Feed/Hook消费订单事件(请参考
    marketplace-order-hook
  • 通用API限流(请参考
    marketplace-rate-limiting

Decision rules

决策规则

External Seller protocol (implemented on the seller host)

外部卖家协议(卖家服务端实现)

  • Fulfillment simulation
    POST /pvt/orderForms/simulation
    . VTEX calls it during product indexation and during checkout. Requests may include only
    items
    (and optionally query params), or the full checkout context:
    items
    ,
    postalCode
    ,
    country
    ,
    clientProfileData
    ,
    shippingData
    ,
    selectedSla
    , etc. Without postal code / profile (typical indexation), the response must still state whether each item is available. With full context, return
    items
    ,
    logisticsInfo
    (one entry per requested item),
    postalCode
    ,
    country
    , and set
    allowMultipleDeliveries
    to
    true
    as required by the contract.
    items[].id
    is the seller SKU id;
    items[].seller
    is the seller id on the marketplace account.
  • Response shape — Each
    logisticsInfo[]
    row aligns with a requested item (
    itemIndex
    ). Include
    slas[]
    with all delivery options (home delivery and pickup-in-point when applicable),
    deliveryChannels[]
    with per-channel stock,
    shipsTo
    , and
    stockBalance
    . SLA fields include
    price
    (shipping per item, in cents),
    shippingEstimate
    /
    shippingEstimateDate
    (e.g.
    5bd
    ,
    30m
    ). Pickup SLAs must include
    pickupStoreInfo
    (address,
    friendlyName
    , etc.).
  • SLA — The simulation handler must respond within 2.5 seconds or the offer is treated as unavailable.
  • Order placement
    POST /pvt/orders
    with a JSON array of orders. For each order, create a reservation in your system. The response must be the same structure with an added
    orderId
    on each order: that value is your reservation id and becomes the
    sellerOrderId
    in later protocol calls (e.g. authorize fulfillment path parameter).
  • Order dispatch (authorize fulfillment) — After marketplace approval, VTEX calls
    POST /pvt/orders/{sellerOrderId}/fulfill
    where
    sellerOrderId
    equals the
    orderId
    you returned at placement. Body includes
    marketplaceOrderId
    and
    marketplaceOrderGroup
    . Convert the reservation to a firm order in your system; response body includes
    date
    ,
    marketplaceOrderId
    ,
    orderId
    (seller reference),
    receipt
    .
  • 履约模拟 ——
    POST /pvt/orderForms/simulation
    。VTEX会在商品索引构建结账流程中调用该接口。请求可能仅包含
    items
    (可选附带查询参数),或者完整的结账上下文:
    items
    postalCode
    country
    clientProfileData
    shippingData
    selectedSla
    等。如果没有邮编/用户信息(典型的索引构建场景),响应仍需要说明每个商品是否可用。如果包含完整上下文,需要返回**
    items
    logisticsInfo
    (每个请求商品对应一条记录)、
    postalCode
    country
    ,并按照协议要求将
    allowMultipleDeliveries
    设为
    true
    items[].id
    卖家SKU ID**;
    items[].seller
    是卖家在市场账户中的卖家ID
  • 响应结构 —— 每个
    logisticsInfo[]
    条目对应一个请求商品(通过
    itemIndex
    关联)。需要包含**
    slas[]
    列出所有配送选项(上门配送和可选的自提点取货**)、
    deliveryChannels[]
    按渠道展示库存、
    shipsTo
    可配送区域,以及
    stockBalance
    库存余额。SLA字段包括
    price
    (每件商品的运费,单位为分)、
    shippingEstimate
    /
    shippingEstimateDate
    (例如
    5bd
    30m
    )。自提SLA必须包含
    pickupStoreInfo
    (地址、
    friendlyName
    友好名称等)。
  • SLA要求 —— 模拟接口必须在2.5秒内响应,否则商品报价会被判定为不可用。
  • 下单 ——
    POST /pvt/orders
    接收JSON数组格式的订单。对于每个订单,在你方系统中创建一条预留记录。响应结构必须与请求一致,每个订单新增**
    orderId
    字段:该值是你方的预留ID,后续协议调用(例如履约授权的路径参数)中将作为
    sellerOrderId
    **使用。
  • 订单发货(履约授权) —— 市场审核通过订单后,VTEX会调用
    POST /pvt/orders/{sellerOrderId}/fulfill
    ,其中**
    sellerOrderId
    等于你下单时返回的
    orderId
    。请求体包含
    marketplaceOrderId
    marketplaceOrderGroup
    。你需要将系统中的预留记录转为正式订单;响应体包含
    date
    marketplaceOrderId
    orderId
    **(卖家侧引用ID)、
    receipt

OMS APIs (seller → marketplace)

OMS API(卖家 → 市场)

  • Send invoices via
    POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice
    . Required fields:
    type
    ,
    invoiceNumber
    ,
    invoiceValue
    (in cents),
    issuanceDate
    , and
    items
    array. Path
    marketplaceOrderId
    is the VTEX marketplace order id, not your reservation id.
  • Use
    type: "Output"
    for sales invoices (shipment) and
    type: "Input"
    for return invoices.
  • Send tracking separately after the carrier provides it, using
    PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}
    .
  • For split shipments, send one invoice per package with only the items in that package. Each
    invoiceValue
    must reflect only its items.
  • Once an order is invoiced, it cannot be canceled without first sending a return invoice (
    type: "Input"
    ).
  • The fulfillment simulation endpoint must respond within 2.5 seconds or the product is considered unavailable.
Architecture / data flow (high level):
text
VTEX Checkout / indexation          External Seller                    VTEX OMS (marketplace)
       │                                   │                                   │
       │── POST /pvt/orderForms/simulation ▶│  Price, stock, SLAs               │
       │◀── 200 + items + logisticsInfo ───│                                   │
       │                                   │                                   │
       │── POST /pvt/orders (array) ───────▶│  Create reservation               │
       │◀── same + orderId (reservation) ─│                                   │
       │                                   │                                   │
       │── POST /pvt/orders/{id}/fulfill ──▶│  Commit / pick pack               │
       │◀── date, marketplaceOrderId, ... ─│                                   │
       │                                   │── POST .../invoice ────────────────▶│
       │                                   │── PATCH .../invoice/{n} ─────────▶│
  • 通过
    POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice
    发送发票。必填字段:
    type
    invoiceNumber
    invoiceValue
    (单位为分)、
    issuanceDate
    items
    数组。路径中的**
    marketplaceOrderId
    **是VTEX市场订单ID,不是你方的预留ID。
  • 销售发票(发货)使用
    type: "Output"
    ,退货发票使用
    type: "Input"
  • 物流商提供跟踪信息后,单独使用
    PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}
    发送跟踪信息。
  • 对于拆分发货的订单,每个包裹对应一张发票,仅包含该包裹内的商品。每个
    invoiceValue
    必须仅对应当前包裹的商品金额。
  • 订单开具发票后,必须先发送退货发票(
    type: "Input"
    )才能取消订单。
  • 履约模拟接口必须在2.5秒内响应,否则商品会被判定为不可用。
架构/数据流(高层级)
text
VTEX 结账/索引构建                外部卖家                          VTEX OMS(市场)
       │                                   │                                   │
       │── POST /pvt/orderForms/simulation ▶│ 价格、库存、SLA                    │
       │◀── 200 + items + logisticsInfo ───│                                   │
       │                                   │                                   │
       │── POST /pvt/orders (数组) ───────▶│ 创建预留记录                       │
       │◀── 原结构 + orderId(预留ID) ───│                                   │
       │                                   │                                   │
       │── POST /pvt/orders/{id}/fulfill ──▶│ 确认/拣货打包                       │
       │◀── date, marketplaceOrderId, ... ─│                                   │
       │                                   │── POST .../invoice ────────────────▶│
       │                                   │── PATCH .../invoice/{n} ─────────▶│

Hard constraints

硬性约束

Constraint: Marketplace order ID in OMS paths

约束:OMS路径中的市场订单ID

Any
{orderId}
in
/api/oms/pvt/orders/{orderId}/...
MUST be the VTEX marketplace order id (OMS), not the
orderId
you returned at
POST /pvt/orders
(reservation id). Map
marketplaceOrderId
from the protocol (placement payload, fulfill body, or events) before calling invoice or tracking APIs.
Why this matters
Using the reservation id in OMS URLs fails to match the marketplace order; invoices and tracking never attach to the customer order.
Detection
If the same variable is used for both
POST /pvt/orders
response
orderId
and
POST .../oms/.../invoice
without mapping → STOP.
Correct
typescript
// reservationId from your POST /pvt/orders response; marketplaceOrderId from VTEX payload
await omsClient.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, payload);
Wrong
typescript
await omsClient.post(`/api/oms/pvt/orders/${reservationId}/invoice`, payload);

/api/oms/pvt/orders/{orderId}/...
中的任何
{orderId}
必须是
VTEX市场订单ID
(OMS侧),而不是你在**
POST /pvt/orders
中返回的
orderId
**(预留ID)。调用发票或跟踪API前,需要从协议数据(下单请求体、履约通知体、事件)中映射得到
marketplaceOrderId
重要性
在OMS URL中使用预留ID会导致无法匹配市场订单,发票和跟踪信息永远无法关联到客户订单。
检测规则
如果
POST /pvt/orders
响应的
orderId
POST .../oms/.../invoice
使用同一个变量且没有做映射 → 停止开发。
正确示例
typescript
// reservationId来自你POST /pvt/orders的响应;marketplaceOrderId来自VTEX的请求体
await omsClient.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, payload);
错误示例
typescript
await omsClient.post(`/api/oms/pvt/orders/${reservationId}/invoice`, payload);

Constraint: Fulfillment simulation contract and latency

约束:履约模拟协议和延迟要求

The seller MUST implement
POST /pvt/orderForms/simulation
to return a valid
items
array for every request. When the request includes checkout context (e.g.
postalCode
,
clientProfileData
,
shippingData
), the response MUST include aligned
logisticsInfo
,
slas
for all relevant modes (including pickup when offered), and
allowMultipleDeliveries
: true
where required. The handler MUST complete within 2.5 seconds.
Why this matters
Incomplete
logisticsInfo
or missing SLAs break checkout shipping selection. Slow responses mark offers unavailable and hurt conversion.
Detection
If simulation returns only
items
with prices but omits
logisticsInfo
when the request had
shippingData
→ warn. If p95 latency approaches 2s without caching → warn.
Correct
typescript
// Pseudocode: branch on whether checkout context is present
if (hasCheckoutContext(req.body)) {
  return res.json({
    country: req.body.country,
    items: pricedItems,
    logisticsInfo: buildLogisticsPerItem(pricedItems, req.body),
    postalCode: req.body.postalCode,
    allowMultipleDeliveries: true,
  });
}
return res.json({ items: availabilityOnlyItems /* + minimal logistics if required */ });
Wrong
typescript
// WRONG: Full checkout body but response omits logisticsInfo / SLAs
res.json({ items: pricedItemsOnly });

卖家必须实现**
POST /pvt/orderForms/simulation
接口,对每个请求返回有效的
items
数组。当请求包含结账上下文(例如
postalCode
clientProfileData
shippingData
)时,响应必须包含对应的
logisticsInfo
、所有相关配送模式的
slas
(如果提供自提则要包含),以及必要的
allowMultipleDeliveries: true
。接口必须在2.5秒**内完成响应。
重要性
logisticsInfo
不完整或缺少SLA会破坏结账时的配送选择流程。响应过慢会导致商品被标记为不可用,损害转化率。
检测规则
如果请求包含
shippingData
但模拟接口仅返回带价格的
items
,缺失
logisticsInfo
→ 告警。如果p95延迟在未做缓存的情况下接近2秒 → 告警。
正确示例
typescript
// 伪代码:根据是否存在结账上下文分支处理
if (hasCheckoutContext(req.body)) {
  return res.json({
    country: req.body.country,
    items: pricedItems,
    logisticsInfo: buildLogisticsPerItem(pricedItems, req.body),
    postalCode: req.body.postalCode,
    allowMultipleDeliveries: true,
  });
}
return res.json({ items: availabilityOnlyItems /* + 协议要求的最小logistics信息 */ });
错误示例
typescript
// 错误:请求是完整结账体但响应缺失logisticsInfo / SLA
res.json({ items: pricedItemsOnly });

Constraint: Order placement must return seller
orderId
(reservation)

约束:下单接口必须返回卖家
orderId
(预留ID)

POST /pvt/orders
accepts a JSON array of orders. The response MUST be the same orders with
orderId
set on each element to your reservation identifier (seller system). VTEX uses that value as
sellerOrderId
in
POST /pvt/orders/{sellerOrderId}/fulfill
.
Why this matters
Omitting or reusing a fake
orderId
breaks the link between marketplace order flow and your reservation and prevents dispatch from routing correctly.
Detection
If the handler returns 200 without adding
orderId
, or returns a single object instead of an array → warn.
Correct
typescript
app.post("/pvt/orders", (req, res) => {
  const orders = req.body as Array<Record<string, unknown>>;
  const out = orders.map((order) => {
    const reservationId = createReservation(order);
    return { ...order, orderId: reservationId, followUpEmail: "" };
  });
  res.json(out);
});
Wrong
typescript
app.post("/pvt/orders", (req, res) => {
  createReservation(req.body);
  res.status(200).send(); // WRONG: missing orderId echo
});

POST /pvt/orders
接收JSON数组
格式的订单。响应必须是相同的订单数组,每个元素都设置**
orderId
为你方的预留标识符**(卖家系统生成)。VTEX会将该值作为**
sellerOrderId
用于
POST /pvt/orders/{sellerOrderId}/fulfill
**请求。
重要性
缺失或使用虚假的
orderId
会破坏市场订单流和你方预留记录之间的关联,导致发货请求无法正确路由。
检测规则
如果接口返回200但没有新增
orderId
,或者返回单个对象而非数组 → 告警。
正确示例
typescript
app.post("/pvt/orders", (req, res) => {
  const orders = req.body as Array<Record<string, unknown>>;
  const out = orders.map((order) => {
    const reservationId = createReservation(order);
    return { ...order, orderId: reservationId, followUpEmail: "" };
  });
  res.json(out);
});
错误示例
typescript
app.post("/pvt/orders", (req, res) => {
  createReservation(req.body);
  res.status(200).send(); // 错误:缺失返回的orderId
});

Constraint: Send Correct Invoice Format with All Required Fields

约束:发送正确的发票格式,包含所有必填字段

The invoice notification MUST include
type
,
invoiceNumber
,
invoiceValue
,
issuanceDate
, and
items
array. The
invoiceValue
MUST be in cents. The
items
array MUST match the items in the order.
Why this matters
Missing required fields cause the API to reject the invoice with 400 Bad Request, leaving the order stuck in "handling" status. Incorrect
invoiceValue
(e.g., using dollars instead of cents) causes financial discrepancies in marketplace reconciliation.
Detection
If you see an invoice notification payload missing
invoiceNumber
,
invoiceValue
,
issuanceDate
, or
items
→ warn about missing required fields. If
invoiceValue
appears to be in dollars (e.g.,
99.90
instead of
9990
) → warn about cents conversion.
Correct
typescript
import axios, { AxiosInstance } from "axios";

interface InvoiceItem {
  id: string;
  quantity: number;
  price: number; // in cents
}

interface InvoicePayload {
  type: "Output" | "Input";
  invoiceNumber: string;
  invoiceValue: number; // total in cents
  issuanceDate: string; // ISO 8601
  invoiceUrl?: string;
  invoiceKey?: string;
  courier?: string;
  trackingNumber?: string;
  trackingUrl?: string;
  items: InvoiceItem[];
}

async function sendInvoiceNotification(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoice: InvoicePayload
): Promise<void> {
  // Validate required fields before sending
  if (!invoice.invoiceNumber) {
    throw new Error("invoiceNumber is required");
  }
  if (!invoice.invoiceValue || invoice.invoiceValue <= 0) {
    throw new Error("invoiceValue must be a positive number in cents");
  }
  if (!invoice.issuanceDate) {
    throw new Error("issuanceDate is required");
  }
  if (!invoice.items || invoice.items.length === 0) {
    throw new Error("items array is required and must not be empty");
  }

  // Warn if invoiceValue looks like it's in dollars instead of cents
  if (invoice.invoiceValue < 100 && invoice.items.length > 0) {
    console.warn(
      `Warning: invoiceValue ${invoice.invoiceValue} seems very low. ` +
        `Ensure it's in cents (e.g., 9990 for $99.90).`
    );
  }

  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, invoice);
}

// Example usage:
async function invoiceOrder(
  client: AxiosInstance,
  marketplaceOrderId: string
): Promise<void> {
  await sendInvoiceNotification(client, marketplaceOrderId, {
    type: "Output",
    invoiceNumber: "NFE-2026-001234",
    invoiceValue: 15990, // $159.90 in cents
    issuanceDate: new Date().toISOString(),
    invoiceUrl: "https://invoices.example.com/NFE-2026-001234.pdf",
    invoiceKey: "35260614388220000199550010000012341000012348",
    items: [
      { id: "123", quantity: 1, price: 9990 },
      { id: "456", quantity: 2, price: 3000 },
    ],
  });
}
Wrong
typescript
// WRONG: Missing required fields, value in dollars instead of cents
async function sendBrokenInvoice(
  client: AxiosInstance,
  marketplaceOrderId: string
): Promise<void> {
  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
    // Missing 'type' field — API may reject or default incorrectly
    invoiceNumber: "001234",
    invoiceValue: 159.9, // WRONG: dollars, not cents — causes financial mismatch
    // Missing 'issuanceDate' — API will reject with 400
    // Missing 'items' — API cannot match invoice to order items
  });
}

发票通知必须包含
type
invoiceNumber
invoiceValue
issuanceDate
items
数组。
invoiceValue
必须以分为单位。
items
数组必须匹配订单中的商品。
重要性
缺失必填字段会导致API返回400 Bad Request拒绝发票,订单会卡在“处理中”状态。
invoiceValue
不正确(例如使用美元而非分)会导致市场对账出现财务差异。
检测规则
如果发票通知请求体缺失
invoiceNumber
invoiceValue
issuanceDate
items
→ 告警缺失必填字段。如果
invoiceValue
看起来是美元格式(例如
99.90
而非
9990
) → 告警需要转换为分单位。
正确示例
typescript
import axios, { AxiosInstance } from "axios";

interface InvoiceItem {
  id: string;
  quantity: number;
  price: number; // 单位为分
}

interface InvoicePayload {
  type: "Output" | "Input";
  invoiceNumber: string;
  invoiceValue: number; // 总金额,单位为分
  issuanceDate: string; // ISO 8601格式
  invoiceUrl?: string;
  invoiceKey?: string;
  courier?: string;
  trackingNumber?: string;
  trackingUrl?: string;
  items: InvoiceItem[];
}

async function sendInvoiceNotification(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoice: InvoicePayload
): Promise<void> {
  // 发送前验证必填字段
  if (!invoice.invoiceNumber) {
    throw new Error("invoiceNumber是必填字段");
  }
  if (!invoice.invoiceValue || invoice.invoiceValue <= 0) {
    throw new Error("invoiceValue必须是正整数,单位为分");
  }
  if (!invoice.issuanceDate) {
    throw new Error("issuanceDate是必填字段");
  }
  if (!invoice.items || invoice.items.length === 0) {
    throw new Error("items数组是必填字段且不能为空");
  }

  // 如果invoiceValue看起来是美元而非分则告警
  if (invoice.invoiceValue < 100 && invoice.items.length > 0) {
    console.warn(
      `警告:invoiceValue ${invoice.invoiceValue} 数值过低。` +
        `请确认单位为分(例如99.90美元对应9990)。`
    );
  }

  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, invoice);
}

// 示例用法:
async function invoiceOrder(
  client: AxiosInstance,
  marketplaceOrderId: string
): Promise<void> {
  await sendInvoiceNotification(client, marketplaceOrderId, {
    type: "Output",
    invoiceNumber: "NFE-2026-001234",
    invoiceValue: 15990, // 159.90美元,单位为分
    issuanceDate: new Date().toISOString(),
    invoiceUrl: "https://invoices.example.com/NFE-2026-001234.pdf",
    invoiceKey: "35260614388220000199550010000012341000012348",
    items: [
      { id: "123", quantity: 1, price: 9990 },
      { id: "456", quantity: 2, price: 3000 },
    ],
  });
}
错误示例
typescript
// 错误:缺失必填字段,金额单位为美元而非分
async function sendBrokenInvoice(
  client: AxiosInstance,
  marketplaceOrderId: string
): Promise<void> {
  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
    // 缺失'type'字段 —— API可能拒绝或默认值错误
    invoiceNumber: "001234",
    invoiceValue: 159.9, // 错误:单位为美元而非分 —— 导致财务不匹配
    // 缺失'issuanceDate' —— API返回400拒绝
    // 缺失'items' —— API无法将发票关联到订单商品
  });
}

Constraint: Update Tracking Promptly After Shipping

约束:发货后及时更新物流跟踪信息

Tracking information MUST be sent as soon as the carrier provides it. Use
PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}
to add tracking to an existing invoice.
Why this matters
Late tracking updates prevent customers from seeing shipment status in the marketplace. The order remains in "invoiced" state instead of progressing to "delivering" and then "delivered". This generates customer support tickets and damages seller reputation.
Detection
If you see tracking information being batched for daily updates instead of sent in real-time → warn about prompt tracking updates. If tracking is included in the initial invoice call but the carrier hasn't provided it yet (hardcoded/empty values) → warn.
Correct
typescript
interface TrackingUpdate {
  courier: string;
  trackingNumber: string;
  trackingUrl?: string;
  isDelivered?: boolean;
}

async function updateOrderTracking(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string,
  tracking: TrackingUpdate
): Promise<void> {
  await client.patch(
    `/api/oms/pvt/orders/${marketplaceOrderId}/invoice/${invoiceNumber}`,
    tracking
  );
}

// Send tracking as soon as carrier provides it
async function onCarrierPickup(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string,
  carrierData: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
  await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
    courier: carrierData.name,
    trackingNumber: carrierData.trackingId,
    trackingUrl: carrierData.trackingUrl,
  });
  console.log(
    `Tracking updated for marketplace order ${marketplaceOrderId}: ${carrierData.trackingId}`
  );
}

// Update delivery status when confirmed
async function onDeliveryConfirmed(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string
): Promise<void> {
  await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
    courier: "",
    trackingNumber: "",
    isDelivered: true,
  });
  console.log(`Marketplace order ${marketplaceOrderId} marked as delivered`);
}
Wrong
typescript
// WRONG: Sending empty/fake tracking data with the invoice
async function invoiceWithFakeTracking(
  client: AxiosInstance,
  marketplaceOrderId: string
): Promise<void> {
  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
    type: "Output",
    invoiceNumber: "NFE-001",
    invoiceValue: 9990,
    issuanceDate: new Date().toISOString(),
    items: [{ id: "123", quantity: 1, price: 9990 }],
    // WRONG: Hardcoded tracking — carrier hasn't picked up yet
    courier: "TBD",
    trackingNumber: "PENDING",
    trackingUrl: "",
  });
  // Customer sees "PENDING" as tracking number — useless information
}

物流商提供跟踪信息后必须立即发送。使用
PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}
为已有发票添加跟踪信息。
重要性
跟踪信息更新不及时会导致客户无法在市场中查看物流状态。订单会停留在“已开票”状态,无法推进到“配送中”和“已送达”。这会增加客户咨询工单,损害卖家声誉。
检测规则
如果跟踪信息按天批量更新而非实时发送 → 告警需要及时更新跟踪信息。如果初始发票调用中就包含跟踪信息但物流商尚未提供(硬编码/空值) → 告警。
正确示例
typescript
interface TrackingUpdate {
  courier: string;
  trackingNumber: string;
  trackingUrl?: string;
  isDelivered?: boolean;
}

async function updateOrderTracking(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string,
  tracking: TrackingUpdate
): Promise<void> {
  await client.patch(
    `/api/oms/pvt/orders/${marketplaceOrderId}/invoice/${invoiceNumber}`,
    tracking
  );
}

// 物流商提供信息后立即发送跟踪信息
async function onCarrierPickup(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string,
  carrierData: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
  await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
    courier: carrierData.name,
    trackingNumber: carrierData.trackingId,
    trackingUrl: carrierData.trackingUrl,
  });
  console.log(
    `市场订单 ${marketplaceOrderId} 跟踪信息已更新:${carrierData.trackingId}`
  );
}

// 确认送达后更新状态
async function onDeliveryConfirmed(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string
): Promise<void> {
  await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
    courier: "",
    trackingNumber: "",
    isDelivered: true,
  });
  console.log(`市场订单 ${marketplaceOrderId} 已标记为已送达`);
}
错误示例
typescript
// 错误:开发票时发送空/虚假的跟踪数据
async function invoiceWithFakeTracking(
  client: AxiosInstance,
  marketplaceOrderId: string
): Promise<void> {
  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
    type: "Output",
    invoiceNumber: "NFE-001",
    invoiceValue: 9990,
    issuanceDate: new Date().toISOString(),
    items: [{ id: "123", quantity: 1, price: 9990 }],
    // 错误:硬编码跟踪信息 —— 物流商尚未取件
    courier: "TBD",
    trackingNumber: "PENDING",
    trackingUrl: "",
  });
  // 客户看到跟踪号为"PENDING" —— 无用信息
}

Constraint: Handle Partial Invoicing for Split Shipments

约束:拆分发货需要处理部分开票

For orders shipped in multiple packages, each shipment MUST have its own invoice with only the items included in that package. The
invoiceValue
MUST reflect only the items in that particular shipment.
Why this matters
Sending a single invoice for the full order value when only partial items are shipped causes financial discrepancies. The marketplace cannot reconcile payments correctly, and the order status may not progress properly.
Detection
If you see a single invoice being sent with the full order value for partial shipments → warn about partial invoicing. If the items array doesn't match the actual items being shipped → warn.
Correct
typescript
interface OrderItem {
  id: string;
  name: string;
  quantity: number;
  price: number; // per unit in cents
}

interface Shipment {
  items: OrderItem[];
  invoiceNumber: string;
}

async function sendPartialInvoices(
  client: AxiosInstance,
  marketplaceOrderId: string,
  shipments: Shipment[]
): Promise<void> {
  for (const shipment of shipments) {
    // Calculate value for only the items in this shipment
    const shipmentValue = shipment.items.reduce(
      (total, item) => total + item.price * item.quantity,
      0
    );

    await sendInvoiceNotification(client, marketplaceOrderId, {
      type: "Output",
      invoiceNumber: shipment.invoiceNumber,
      invoiceValue: shipmentValue,
      issuanceDate: new Date().toISOString(),
      items: shipment.items.map((item) => ({
        id: item.id,
        quantity: item.quantity,
        price: item.price,
      })),
    });

    console.log(
      `Partial invoice ${shipment.invoiceNumber} sent for marketplace order ${marketplaceOrderId}: ` +
        `${shipment.items.length} items, value=${shipmentValue}`
    );
  }
}

// Example: Order with 3 items shipped in 2 packages
await sendPartialInvoices(client, "vtex-marketplace-order-id-12345", [
  {
    invoiceNumber: "NFE-001-A",
    items: [
      { id: "sku-1", name: "Laptop", quantity: 1, price: 250000 },
    ],
  },
  {
    invoiceNumber: "NFE-001-B",
    items: [
      { id: "sku-2", name: "Mouse", quantity: 1, price: 5000 },
      { id: "sku-3", name: "Keyboard", quantity: 1, price: 12000 },
    ],
  },
]);
Wrong
typescript
// WRONG: Sending full order value for partial shipment
async function wrongPartialInvoice(
  client: AxiosInstance,
  marketplaceOrderId: string,
  totalOrderValue: number,
  shippedItems: OrderItem[]
): Promise<void> {
  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
    type: "Output",
    invoiceNumber: "NFE-001-A",
    invoiceValue: totalOrderValue, // WRONG: Full order value, not partial
    issuanceDate: new Date().toISOString(),
    items: shippedItems.map((item) => ({
      id: item.id,
      quantity: item.quantity,
      price: item.price,
    })),
    // invoiceValue doesn't match sum of items — financial mismatch
  });
}
对于分多个包裹发货的订单,每个包裹必须对应单独的发票,仅包含该包裹内的商品。
invoiceValue
必须仅反映当前包裹的商品金额。
重要性
仅部分商品发货时就发送全额发票会导致财务差异。市场无法正确对账,订单状态也可能无法正常推进。
检测规则
如果拆分发货时发送全额发票 → 告警需要部分开票。如果
items
数组与实际发货商品不匹配 → 告警。
正确示例
typescript
interface OrderItem {
  id: string;
  name: string;
  quantity: number;
  price: number; // 单价,单位为分
}

interface Shipment {
  items: OrderItem[];
  invoiceNumber: string;
}

async function sendPartialInvoices(
  client: AxiosInstance,
  marketplaceOrderId: string,
  shipments: Shipment[]
): Promise<void> {
  for (const shipment of shipments) {
    // 仅计算当前包裹的商品金额
    const shipmentValue = shipment.items.reduce(
      (total, item) => total + item.price * item.quantity,
      0
    );

    await sendInvoiceNotification(client, marketplaceOrderId, {
      type: "Output",
      invoiceNumber: shipment.invoiceNumber,
      invoiceValue: shipmentValue,
      issuanceDate: new Date().toISOString(),
      items: shipment.items.map((item) => ({
        id: item.id,
        quantity: item.quantity,
        price: item.price,
      })),
    });

    console.log(
      `市场订单 ${marketplaceOrderId} 的部分发票 ${shipment.invoiceNumber} 已发送:` +
        `${shipment.items.length}件商品,金额=${shipmentValue}`
    );
  }
}

// 示例:包含3件商品的订单分2个包裹发货
await sendPartialInvoices(client, "vtex-marketplace-order-id-12345", [
  {
    invoiceNumber: "NFE-001-A",
    items: [
      { id: "sku-1", name: "笔记本电脑", quantity: 1, price: 250000 },
    ],
  },
  {
    invoiceNumber: "NFE-001-B",
    items: [
      { id: "sku-2", name: "鼠标", quantity: 1, price: 5000 },
      { id: "sku-3", name: "键盘", quantity: 1, price: 12000 },
    ],
  },
]);
错误示例
typescript
// 错误:部分发货时发送全额发票
async function wrongPartialInvoice(
  client: AxiosInstance,
  marketplaceOrderId: string,
  totalOrderValue: number,
  shippedItems: OrderItem[]
): Promise<void> {
  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
    type: "Output",
    invoiceNumber: "NFE-001-A",
    invoiceValue: totalOrderValue, // 错误:全额订单金额,非部分金额
    issuanceDate: new Date().toISOString(),
    items: shippedItems.map((item) => ({
      id: item.id,
      quantity: item.quantity,
      price: item.price,
    })),
    // invoiceValue与商品金额总和不匹配 —— 财务不匹配
  });
}

Preferred pattern

推荐模式

Implement fulfillment simulation

实现履约模拟

Register
POST /pvt/orderForms/simulation
. Parse
items[]
(
id
= seller SKU,
seller
= seller id on marketplace). If the body has no
postalCode
/
clientProfileData
, treat as indexation: return availability (and minimal logistics if your contract requires it). If the body includes checkout fields, build
logisticsInfo
per
itemIndex
, populate
slas
(delivery + pickup-in-point with
pickupStoreInfo
when applicable),
deliveryChannels
,
stockBalance
, set
country
/
postalCode
, and
allowMultipleDeliveries
: true
. Keep CPU and I/O bounded so you stay under 2.5s.
typescript
import { RequestHandler } from "express";

const fulfillmentSimulation: RequestHandler = async (req, res) => {
  const body = req.body as Record<string, unknown>;
  const items = body.items as Array<{ id: string; quantity: number; seller: string }>;
  const hasCheckoutContext = Boolean(body.postalCode && body.shippingData);

  const pricedItems = await priceAndStockForItems(items);

  if (!hasCheckoutContext) {
    res.json({ items: pricedItems, logisticsInfo: minimalLogistics(pricedItems) });
    return;
  }

  res.json({
    country: body.country,
    postalCode: body.postalCode,
    items: pricedItems,
    logisticsInfo: buildFullLogistics(pricedItems, body),
    allowMultipleDeliveries: true,
  });
};

function minimalLogistics(
  pricedItems: Array<{ id: string; requestIndex: number }>
): unknown[] {
  return pricedItems.map((_, i) => ({
    itemIndex: i,
    quantity: 1,
    shipsTo: ["USA"],
    slas: [],
    stockBalance: "",
    deliveryChannels: [{ id: "delivery", stockBalance: "" }],
  }));
}

function buildFullLogistics(
  pricedItems: Array<{ id: string; requestIndex: number }>,
  _checkoutBody: unknown
): unknown[] {
  // Replace with SLA / carrier rules derived from checkoutBody
  return pricedItems.map((_, i) => ({
    itemIndex: i,
    quantity: 1,
    shipsTo: ["USA"],
    slas: [],
    stockBalance: 0,
    deliveryChannels: [],
  }));
}

async function priceAndStockForItems(
  items: Array<{ id: string; quantity: number; seller: string }>
): Promise<Array<Record<string, unknown>>> {
  return items.map((item, requestIndex) => ({
    id: item.id,
    quantity: item.quantity,
    seller: item.seller,
    measurementUnit: "un",
    merchantName: null,
    price: 0,
    priceTags: [],
    priceValidUntil: null,
    requestIndex,
    unitMultiplier: 1,
    attachmentOfferings: [],
  }));
}
注册**
POST /pvt/orderForms/simulation
接口。解析
items[]
id
=卖家SKU,
seller
=市场侧卖家ID)。如果请求体没有
postalCode
/
clientProfileData
,判定为
索引构建场景:返回可用性信息(以及协议要求的最小logistics信息)。如果请求体包含结账字段,按
itemIndex
构建
logisticsInfo
,填充
slas
(配送+自提,包含
pickupStoreInfo
如果适用)、
deliveryChannels
stockBalance
,设置
country
/
postalCode
,以及
allowMultipleDeliveries: true
。控制CPU和IO耗时,确保响应低于2.5秒**。
typescript
import { RequestHandler } from "express";

const fulfillmentSimulation: RequestHandler = async (req, res) => {
  const body = req.body as Record<string, unknown>;
  const items = body.items as Array<{ id: string; quantity: number; seller: string }>;
  const hasCheckoutContext = Boolean(body.postalCode && body.shippingData);

  const pricedItems = await priceAndStockForItems(items);

  if (!hasCheckoutContext) {
    res.json({ items: pricedItems, logisticsInfo: minimalLogistics(pricedItems) });
    return;
  }

  res.json({
    country: body.country,
    postalCode: body.postalCode,
    items: pricedItems,
    logisticsInfo: buildFullLogistics(pricedItems, body),
    allowMultipleDeliveries: true,
  });
};

function minimalLogistics(
  pricedItems: Array<{ id: string; requestIndex: number }>
): unknown[] {
  return pricedItems.map((_, i) => ({
    itemIndex: i,
    quantity: 1,
    shipsTo: ["USA"],
    slas: [],
    stockBalance: "",
    deliveryChannels: [{ id: "delivery", stockBalance: "" }],
  }));
}

function buildFullLogistics(
  pricedItems: Array<{ id: string; requestIndex: number }>,
  _checkoutBody: unknown
): unknown[] {
  // 替换为基于checkoutBody的SLA/物流商规则
  return pricedItems.map((_, i) => ({
    itemIndex: i,
    quantity: 1,
    shipsTo: ["USA"],
    slas: [],
    stockBalance: 0,
    deliveryChannels: [],
  }));
}

async function priceAndStockForItems(
  items: Array<{ id: string; quantity: number; seller: string }>
): Promise<Array<Record<string, unknown>>> {
  return items.map((item, requestIndex) => ({
    id: item.id,
    quantity: item.quantity,
    seller: item.seller,
    measurementUnit: "un",
    merchantName: null,
    price: 0,
    priceTags: [],
    priceValidUntil: null,
    requestIndex,
    unitMultiplier: 1,
    attachmentOfferings: [],
  }));
}

Implement order placement (reservation)

实现下单(预留)接口

Register
POST /pvt/orders
. Body is an array. Persist each order as a reservation; respond with the same objects plus
orderId
(reservation key) and typically
followUpEmail
. That
orderId
is what VTEX passes as
sellerOrderId
on
POST /pvt/orders/{sellerOrderId}/fulfill
.
typescript
import { RequestHandler } from "express";

function createReservation(_order: Record<string, unknown>): string {
  return `res-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}

const orderPlacement: RequestHandler = (req, res) => {
  const orders = req.body as Array<Record<string, unknown>>;
  const out = orders.map((order) => ({
    ...order,
    orderId: createReservation(order),
    followUpEmail: "",
  }));
  res.json(out);
};
注册**
POST /pvt/orders
接口。请求体是数组**。将每个订单持久化为预留记录;响应返回相同的对象,新增**
orderId
(预留记录key)和通常的
followUpEmail
字段。该
orderId
就是VTEX在
POST /pvt/orders/{sellerOrderId}/fulfill
中传入的
sellerOrderId
**。
typescript
import { RequestHandler } from "express";

function createReservation(_order: Record<string, unknown>): string {
  return `res-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}

const orderPlacement: RequestHandler = (req, res) => {
  const orders = req.body as Array<Record<string, unknown>>;
  const out = orders.map((order) => ({
    ...order,
    orderId: createReservation(order),
    followUpEmail: "",
  }));
  res.json(out);
};

Implement the Authorize Fulfillment Endpoint

实现履约授权接口

The marketplace calls this endpoint when payment is approved.
typescript
import express, { RequestHandler } from "express";

interface FulfillOrderRequest {
  marketplaceOrderId: string;
  marketplaceOrderGroup: string;
}

interface OrderMapping {
  /** Same id returned as orderId from POST /pvt/orders (reservation) */
  sellerOrderId: string;
  marketplaceOrderId: string;
  items: OrderItem[];
  status: string;
}

// Store for order mappings — use a real database in production
const orderStore = new Map<string, OrderMapping>();

const authorizeFulfillmentHandler: RequestHandler = async (req, res) => {
  const sellerOrderId = req.params.sellerOrderId;
  const { marketplaceOrderId, marketplaceOrderGroup }: FulfillOrderRequest = req.body;

  console.log(
    `Fulfillment authorized: reservation=${sellerOrderId}, marketplaceOrder=${marketplaceOrderId}, group=${marketplaceOrderGroup}`
  );

  // Store the marketplace order ID mapping
  const order = orderStore.get(sellerOrderId);
  if (!order) {
    res.status(404).json({ error: "Order not found" });
    return;
  }

  order.marketplaceOrderId = marketplaceOrderId;
  order.status = "fulfillment_authorized";
  orderStore.set(sellerOrderId, order);

  // Trigger fulfillment process asynchronously
  enqueueFulfillment(sellerOrderId).catch(console.error);

  res.status(200).json({
    date: new Date().toISOString(),
    marketplaceOrderId,
    // Echo seller reservation id (same as path param / placement orderId)
    orderId: sellerOrderId,
    receipt: null,
  });
};

async function enqueueFulfillment(sellerOrderId: string): Promise<void> {
  console.log(`Enqueued fulfillment for ${sellerOrderId}`);
}

const app = express();
app.use(express.json());
app.post("/pvt/orders/:sellerOrderId/fulfill", authorizeFulfillmentHandler);
支付审核通过后市场会调用该接口。
typescript
import express, { RequestHandler } from "express";

interface FulfillOrderRequest {
  marketplaceOrderId: string;
  marketplaceOrderGroup: string;
}

interface OrderMapping {
  /** 与POST /pvt/orders返回的orderId一致(预留ID) */
  sellerOrderId: string;
  marketplaceOrderId: string;
  items: OrderItem[];
  status: string;
}

// 订单映射存储 —— 生产环境使用真实数据库
const orderStore = new Map<string, OrderMapping>();

const authorizeFulfillmentHandler: RequestHandler = async (req, res) => {
  const sellerOrderId = req.params.sellerOrderId;
  const { marketplaceOrderId, marketplaceOrderGroup }: FulfillOrderRequest = req.body;

  console.log(
    `履约已授权:预留ID=${sellerOrderId}, 市场订单ID=${marketplaceOrderId}, 订单组=${marketplaceOrderGroup}`
  );

  // 存储市场订单ID映射
  const order = orderStore.get(sellerOrderId);
  if (!order) {
    res.status(404).json({ error: "订单不存在" });
    return;
  }

  order.marketplaceOrderId = marketplaceOrderId;
  order.status = "fulfillment_authorized";
  orderStore.set(sellerOrderId, order);

  // 异步触发履约流程
  enqueueFulfillment(sellerOrderId).catch(console.error);

  res.status(200).json({
    date: new Date().toISOString(),
    marketplaceOrderId,
    // 返回卖家预留ID(与路径参数/下单返回的orderId一致)
    orderId: sellerOrderId,
    receipt: null,
  });
};

async function enqueueFulfillment(sellerOrderId: string): Promise<void> {
  console.log(`已将${sellerOrderId}加入履约队列`);
}

const app = express();
app.use(express.json());
app.post("/pvt/orders/:sellerOrderId/fulfill", authorizeFulfillmentHandler);

Send Invoice After Fulfillment

履约完成后发送发票

Once the order is packed and the invoice is generated, send the invoice notification.
typescript
async function fulfillAndInvoice(
  client: AxiosInstance,
  order: OrderMapping
): Promise<void> {
  // Generate invoice from your invoicing system
  const invoice = await generateInvoice(order);

  // Send invoice notification to VTEX marketplace
  await sendInvoiceNotification(client, order.marketplaceOrderId, {
    type: "Output",
    invoiceNumber: invoice.number,
    invoiceValue: invoice.totalCents,
    issuanceDate: invoice.issuedAt.toISOString(),
    invoiceUrl: invoice.pdfUrl,
    invoiceKey: invoice.accessKey,
    items: order.items.map((item) => ({
      id: item.id,
      quantity: item.quantity,
      price: item.price,
    })),
  });

  console.log(
    `Invoice ${invoice.number} sent for order ${order.marketplaceOrderId}`
  );
}

async function generateInvoice(order: OrderMapping): Promise<{
  number: string;
  totalCents: number;
  issuedAt: Date;
  pdfUrl: string;
  accessKey: string;
}> {
  const totalCents = order.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  return {
    number: `NFE-${Date.now()}`,
    totalCents,
    issuedAt: new Date(),
    pdfUrl: `https://invoices.example.com/NFE-${Date.now()}.pdf`,
    accessKey: "35260614388220000199550010000012341000012348",
  };
}
订单打包完成且发票生成后,发送发票通知。
typescript
async function fulfillAndInvoice(
  client: AxiosInstance,
  order: OrderMapping
): Promise<void> {
  // 从你的发票系统生成发票
  const invoice = await generateInvoice(order);

  // 向VTEX市场发送发票通知
  await sendInvoiceNotification(client, order.marketplaceOrderId, {
    type: "Output",
    invoiceNumber: invoice.number,
    invoiceValue: invoice.totalCents,
    issuanceDate: invoice.issuedAt.toISOString(),
    invoiceUrl: invoice.pdfUrl,
    invoiceKey: invoice.accessKey,
    items: order.items.map((item) => ({
      id: item.id,
      quantity: item.quantity,
      price: item.price,
    })),
  });

  console.log(
    `订单${order.marketplaceOrderId}的发票${invoice.number}已发送`
  );
}

async function generateInvoice(order: OrderMapping): Promise<{
  number: string;
  totalCents: number;
  issuedAt: Date;
  pdfUrl: string;
  accessKey: string;
}> {
  const totalCents = order.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  return {
    number: `NFE-${Date.now()}`,
    totalCents,
    issuedAt: new Date(),
    pdfUrl: `https://invoices.example.com/NFE-${Date.now()}.pdf`,
    accessKey: "35260614388220000199550010000012341000012348",
  };
}

Send Tracking When Carrier Picks Up

物流商取件后发送跟踪信息

typescript
async function handleCarrierPickup(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string,
  carrier: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
  await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
    courier: carrier.name,
    trackingNumber: carrier.trackingId,
    trackingUrl: carrier.trackingUrl,
  });

  console.log(
    `Tracking ${carrier.trackingId} sent for marketplace order ${marketplaceOrderId}`
  );
}
typescript
async function handleCarrierPickup(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string,
  carrier: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
  await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
    courier: carrier.name,
    trackingNumber: carrier.trackingId,
    trackingUrl: carrier.trackingUrl,
  });

  console.log(
    `市场订单${marketplaceOrderId}的跟踪信息${carrier.trackingId}已发送`
  );
}

Confirm Delivery

确认送达

typescript
async function handleDeliveryConfirmation(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string
): Promise<void> {
  await client.patch(
    `/api/oms/pvt/orders/${marketplaceOrderId}/invoice/${invoiceNumber}`,
    {
      isDelivered: true,
      courier: "",
      trackingNumber: "",
    }
  );

  console.log(`Marketplace order ${marketplaceOrderId} marked as delivered`);
}
typescript
async function handleDeliveryConfirmation(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string
): Promise<void> {
  await client.patch(
    `/api/oms/pvt/orders/${marketplaceOrderId}/invoice/${invoiceNumber}`,
    {
      isDelivered: true,
      courier: "",
      trackingNumber: "",
    }
  );

  console.log(`市场订单${marketplaceOrderId}已标记为已送达`);
}

Complete Example

完整示例

typescript
import axios, { AxiosInstance } from "axios";

function createMarketplaceClient(
  accountName: string,
  appKey: string,
  appToken: string
): AxiosInstance {
  return axios.create({
    baseURL: `https://${accountName}.vtexcommercestable.com.br`,
    headers: {
      "Content-Type": "application/json",
      "X-VTEX-API-AppKey": appKey,
      "X-VTEX-API-AppToken": appToken,
    },
    timeout: 10000,
  });
}

async function completeFulfillmentFlow(
  client: AxiosInstance,
  order: OrderMapping
): Promise<void> {
  // 1. Fulfill and invoice
  await fulfillAndInvoice(client, order);

  // 2. When carrier picks up, send tracking
  const carrierData = await waitForCarrierPickup(order.sellerOrderId);
  const invoice = await getLatestInvoice(order.sellerOrderId);

  await handleCarrierPickup(
    client,
    order.marketplaceOrderId,
    invoice.number,
    carrierData
  );

  // 3. When delivered, confirm
  await waitForDeliveryConfirmation(order.sellerOrderId);
  await handleDeliveryConfirmation(
    client,
    order.marketplaceOrderId,
    invoice.number
  );
}

async function waitForCarrierPickup(
  sellerOrderId: string
): Promise<{ name: string; trackingId: string; trackingUrl: string }> {
  // Replace with actual carrier integration
  return {
    name: "Correios",
    trackingId: "BR123456789",
    trackingUrl: "https://tracking.example.com/BR123456789",
  };
}

async function getLatestInvoice(
  sellerOrderId: string
): Promise<{ number: string }> {
  // Replace with actual invoice lookup
  return { number: `NFE-${sellerOrderId}` };
}

async function waitForDeliveryConfirmation(
  sellerOrderId: string
): Promise<void> {
  // Replace with actual delivery confirmation logic
  console.log(`Waiting for delivery confirmation: ${sellerOrderId}`);
}
typescript
import axios, { AxiosInstance } from "axios";

function createMarketplaceClient(
  accountName: string,
  appKey: string,
  appToken: string
): AxiosInstance {
  return axios.create({
    baseURL: `https://${accountName}.vtexcommercestable.com.br`,
    headers: {
      "Content-Type": "application/json",
      "X-VTEX-API-AppKey": appKey,
      "X-VTEX-API-AppToken": appToken,
    },
    timeout: 10000,
  });
}

async function completeFulfillmentFlow(
  client: AxiosInstance,
  order: OrderMapping
): Promise<void> {
  // 1. 履约并开发票
  await fulfillAndInvoice(client, order);

  // 2. 物流商取件后发送跟踪信息
  const carrierData = await waitForCarrierPickup(order.sellerOrderId);
  const invoice = await getLatestInvoice(order.sellerOrderId);

  await handleCarrierPickup(
    client,
    order.marketplaceOrderId,
    invoice.number,
    carrierData
  );

  // 3. 确认送达后更新状态
  await waitForDeliveryConfirmation(order.sellerOrderId);
  await handleDeliveryConfirmation(
    client,
    order.marketplaceOrderId,
    invoice.number
  );
}

async function waitForCarrierPickup(
  sellerOrderId: string
): Promise<{ name: string; trackingId: string; trackingUrl: string }> {
  // 替换为真实的物流商集成
  return {
    name: "Correios",
    trackingId: "BR123456789",
    trackingUrl: "https://tracking.example.com/BR123456789",
  };
}

async function getLatestInvoice(
  sellerOrderId: string
): Promise<{ number: string }> {
  // 替换为真实的发票查询逻辑
  return { number: `NFE-${sellerOrderId}` };
}

async function waitForDeliveryConfirmation(
  sellerOrderId: string
): Promise<void> {
  // 替换为真实的送达确认逻辑
  console.log(`等待送达确认:${sellerOrderId}`);
}

Common failure modes

常见失败模式

  • Treating indexation simulation like checkout. The same endpoint receives minimal bodies (no customer location) during indexation and rich bodies during checkout. Returning checkout-grade
    logisticsInfo
    for minimal calls can be unnecessary work; returning only prices for checkout calls without SLAs and
    logisticsInfo
    breaks shipping selection.
  • Omitting
    orderId
    on order placement.
    VTEX expects an array response echoing each order with
    orderId
    set to your reservation. Empty 200 responses or missing
    orderId
    strand the order pipeline.
  • Using reservation
    orderId
    in OMS invoice URLs.
    After placement, you must use
    marketplaceOrderId
    from the protocol when calling
    /api/oms/pvt/orders/...
    . Confusing the two ids produces 404 or silent failure on invoice/tracking.
  • Sending invoice before fulfillment authorization. The seller sends an invoice notification immediately when the order is placed, before receiving the Authorize Fulfillment callback from the marketplace. Payment may still be pending or under review. Invoicing before authorization can result in the invoice being rejected or the order being in an inconsistent state. Only send the invoice after receiving
    POST /pvt/orders/{sellerOrderId}/fulfill
    .
  • Not handling return invoices for cancellation. A seller tries to cancel an invoiced order by calling the Cancel Order endpoint directly without first sending a return invoice. Once an order is in "invoiced" status, it cannot be canceled without a return invoice (
    type: "Input"
    ). The Cancel Order API will reject the request.
typescript
// Correct: Send return invoice before canceling an invoiced order
async function cancelInvoicedOrder(
  client: AxiosInstance,
  marketplaceOrderId: string,
  originalItems: InvoiceItem[],
  originalInvoiceValue: number
): Promise<void> {
  // Step 1: Send return invoice (type: "Input")
  await sendInvoiceNotification(client, marketplaceOrderId, {
    type: "Input", // Return invoice
    invoiceNumber: `RET-${Date.now()}`,
    invoiceValue: originalInvoiceValue,
    issuanceDate: new Date().toISOString(),
    items: originalItems,
  });

  // Step 2: Now cancel the order
  await client.post(
    `/api/marketplace/pvt/orders/${marketplaceOrderId}/cancel`,
    { reason: "Customer requested return" }
  );
}
  • Fulfillment simulation exceeding the 2.5-second 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.
  • 将索引构建模拟等同于结账模拟。同一个接口在索引构建时接收极简请求体(无客户位置信息),在结账时接收完整请求体。对极简请求返回完整的
    logisticsInfo
    是不必要的工作;对结账请求仅返回价格而缺失SLA和
    logisticsInfo
    会破坏配送选择流程。
  • 下单响应缺失
    orderId
    。VTEX期望响应是数组,每个订单都设置
    orderId
    为你方的预留ID。空的200响应或缺失
    orderId
    会导致订单流程中断。
  • 在OMS发票URL中使用预留
    orderId
    。下单后调用**
    /api/oms/pvt/orders/...
    时必须使用协议中返回的
    marketplaceOrderId
    **。混淆两个ID会导致发票/跟踪请求返回404或静默失败。
  • 履约授权前发送发票。卖家下单后立即发送发票通知,未收到市场的履约授权回调。此时支付可能仍在处理或审核中。授权前开票会导致发票被拒绝,订单状态不一致。仅在收到
    POST /pvt/orders/{sellerOrderId}/fulfill
    后再发送发票。
  • 取消已开票订单未处理退货发票。卖家直接调用取消订单接口取消已开票订单,未先发送退货发票。订单处于“已开票”状态时,没有退货发票(
    type: "Input"
    )无法取消。取消订单API会拒绝请求。
typescript
// 正确:取消已开票订单前先发送退货发票
async function cancelInvoicedOrder(
  client: AxiosInstance,
  marketplaceOrderId: string,
  originalItems: InvoiceItem[],
  originalInvoiceValue: number
): Promise<void> {
  // 步骤1:发送退货发票(type: "Input")
  await sendInvoiceNotification(client, marketplaceOrderId, {
    type: "Input", // 退货发票
    invoiceNumber: `RET-${Date.now()}`,
    invoiceValue: originalInvoiceValue,
    issuanceDate: new Date().toISOString(),
    items: originalItems,
  });

  // 步骤2:现在可以取消订单
  await client.post(
    `/api/marketplace/pvt/orders/${marketplaceOrderId}/cancel`,
    { reason: "客户申请退货" }
  );
}
  • 履约模拟超过2.5秒超时。卖家的履约模拟接口执行复杂的数据库查询或外部API调用,超过响应时间限制。VTEX市场最多等待2.5秒的履约模拟响应。超时后商品会被判定为不可用/未激活,不会在店铺前台或结账页面展示。请提前缓存价格和库存数据。

Review checklist

审核清单

  • Does
    POST /pvt/orderForms/simulation
    handle both minimal (indexation) and checkout-shaped requests, returning
    logisticsInfo
    and
    slas
    when context is present?
  • Does
    POST /pvt/orders
    accept an array and return each order with
    orderId
    (reservation id)?
  • Does
    POST /pvt/orders/{sellerOrderId}/fulfill
    read
    marketplaceOrderId
    and
    marketplaceOrderGroup
    and match
    sellerOrderId
    to the reservation from placement?
  • Are OMS
    /invoice
    and
    PATCH .../invoice
    calls using
    marketplaceOrderId
    , not the reservation id?
  • Does the seller only begin physical fulfillment after receiving the Authorize Fulfillment callback?
  • For IO/BFF connectors: are caching and route choices aligned with vtex-io skills (simulation SLA, data scope)?
  • Does the invoice payload include all required fields (
    type
    ,
    invoiceNumber
    ,
    invoiceValue
    ,
    issuanceDate
    ,
    items
    )?
  • Is
    invoiceValue
    in cents (not dollars)?
  • Is tracking sent separately after the carrier provides real data (not hardcoded placeholders)?
  • For split shipments, does each invoice cover only its package's items and value?
  • Is cancellation of invoiced orders handled via return invoice (
    type: "Input"
    ) first?
  • Does the fulfillment simulation endpoint respond within 2.5 seconds?
  • POST /pvt/orderForms/simulation
    是否同时处理极简(索引构建)和结账类型的请求,上下文存在时返回
    logisticsInfo
    slas
  • POST /pvt/orders
    是否接收数组,并为每个订单返回带
    orderId
    (预留ID)的响应?
  • POST /pvt/orders/{sellerOrderId}/fulfill
    是否读取
    marketplaceOrderId
    marketplaceOrderGroup
    ,并将**
    sellerOrderId
    **与下单时的预留记录匹配?
  • OMS **
    /invoice
    PATCH .../invoice
    调用是否使用
    marketplaceOrderId
    **而非预留ID?
  • 卖家是否仅在收到履约授权回调后才开始物理履约?
  • 对于IO/BFF连接器:缓存路由选择是否符合vtex-io技能要求(模拟SLA、数据范围)?
  • 发票请求体是否包含所有必填字段(
    type
    invoiceNumber
    invoiceValue
    issuanceDate
    items
    )?
  • invoiceValue
    是否以分为单位(而非美元)?
  • 跟踪信息是否在物流商提供真实数据后单独发送(而非硬编码占位符)?
  • 拆分发货时,每张发票是否仅对应当前包裹的商品和金额?
  • 取消已开票订单是否先发送退货发票(
    type: "Input"
    )?
  • 履约模拟接口是否在2.5秒内响应?

Reference

参考

VTEX also maintains an open reference implementation for the External Seller service under the
vtex-apps/external-seller-example
GitHub repository (useful as a scaffold; align behavior with the official protocol docs above).
VTEX还在**
vtex-apps/external-seller-example
GitHub仓库维护了外部卖家服务的开源参考实现**(可作为脚手架使用;请以上面的官方协议文档为准对齐行为)。