marketplace-fulfillment
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFulfillment, 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 (indexation and/or checkout — with or without customer context)
POST /pvt/orderForms/simulation - Implementing (order placement — create reservation; return
POST /pvt/orders= reservation id in your system)orderId - Handling (order dispatch after approval — path uses the same id you returned as
POST /pvt/orders/{sellerOrderId}/fulfillat placement)orderId - Sending invoice notifications via (marketplace order id in the path)
POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice - 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**为你方系统的预留ID)orderId - 处理 请求(订单审核通过后发货——路径使用的ID与你下单时返回的
POST /pvt/orders/{sellerOrderId}/fulfill完全一致)orderId - 通过 发送发票通知(路径参数为市场订单ID)
POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice - 通过 更新物流跟踪信息
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 — . VTEX calls it during product indexation and during checkout. Requests may include only
POST /pvt/orderForms/simulation(and optionally query params), or the full checkout context:items,items,postalCode,country,clientProfileData,shippingData, etc. Without postal code / profile (typical indexation), the response must still state whether each item is available. With full context, returnselectedSla,items(one entry per requested item),logisticsInfo,postalCode, and setcountrytoallowMultipleDeliveriesas required by the contract.trueis the seller SKU id;items[].idis the seller id on the marketplace account.items[].seller - Response shape — Each row aligns with a requested item (
logisticsInfo[]). IncludeitemIndexwith all delivery options (home delivery and pickup-in-point when applicable),slas[]with per-channel stock,deliveryChannels[], andshipsTo. SLA fields includestockBalance(shipping per item, in cents),price/shippingEstimate(e.g.shippingEstimateDate,5bd). Pickup SLAs must include30m(address,pickupStoreInfo, etc.).friendlyName - SLA — The simulation handler must respond within 2.5 seconds or the offer is treated as unavailable.
- Order placement — 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
POST /pvt/orderson each order: that value is your reservation id and becomes theorderIdin later protocol calls (e.g. authorize fulfillment path parameter).sellerOrderId - Order dispatch (authorize fulfillment) — After marketplace approval, VTEX calls where
POST /pvt/orders/{sellerOrderId}/fulfillequals thesellerOrderIdyou returned at placement. Body includesorderIdandmarketplaceOrderId. Convert the reservation to a firm order in your system; response body includesmarketplaceOrderGroup,date,marketplaceOrderId(seller reference),orderId.receipt
- 履约模拟 —— 。VTEX会在商品索引构建和结账流程中调用该接口。请求可能仅包含
POST /pvt/orderForms/simulation(可选附带查询参数),或者完整的结账上下文:items、items、postalCode、country、clientProfileData、shippingData等。如果没有邮编/用户信息(典型的索引构建场景),响应仍需要说明每个商品是否可用。如果包含完整上下文,需要返回**selectedSla、items(每个请求商品对应一条记录)、logisticsInfo、postalCode,并按照协议要求将country设为allowMultipleDeliveries。true是卖家SKU ID**;items[].id是卖家在市场账户中的卖家ID。items[].seller - 响应结构 —— 每个条目对应一个请求商品(通过
logisticsInfo[]关联)。需要包含**itemIndex列出所有配送选项(上门配送和可选的自提点取货**)、slas[]按渠道展示库存、deliveryChannels[]可配送区域,以及shipsTo库存余额。SLA字段包括stockBalance(每件商品的运费,单位为分)、price/shippingEstimate(例如shippingEstimateDate、5bd)。自提SLA必须包含30m(地址、pickupStoreInfo友好名称等)。friendlyName - SLA要求 —— 模拟接口必须在2.5秒内响应,否则商品报价会被判定为不可用。
- 下单 —— 接收JSON数组格式的订单。对于每个订单,在你方系统中创建一条预留记录。响应结构必须与请求一致,每个订单新增**
POST /pvt/orders字段:该值是你方的预留ID,后续协议调用(例如履约授权的路径参数)中将作为orderId**使用。sellerOrderId - 订单发货(履约授权) —— 市场审核通过订单后,VTEX会调用,其中**
POST /pvt/orders/{sellerOrderId}/fulfill等于你下单时返回的sellerOrderId。请求体包含orderId和marketplaceOrderId。你需要将系统中的预留记录转为正式订单;响应体包含marketplaceOrderGroup、date、marketplaceOrderId**(卖家侧引用ID)、orderId。receipt
OMS APIs (seller → marketplace)
OMS API(卖家 → 市场)
- Send invoices via . Required fields:
POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice,type,invoiceNumber(in cents),invoiceValue, andissuanceDatearray. Pathitemsis the VTEX marketplace order id, not your reservation id.marketplaceOrderId - Use for sales invoices (shipment) and
type: "Output"for return invoices.type: "Input" - 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 must reflect only its items.
invoiceValue - 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**是VTEX市场订单ID,不是你方的预留ID。marketplaceOrderId - 销售发票(发货)使用,退货发票使用
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 in MUST be the VTEX marketplace order id (OMS), not the you returned at (reservation id). Map from the protocol (placement payload, fulfill body, or events) before calling invoice or tracking APIs.
{orderId}/api/oms/pvt/orders/{orderId}/...orderIdPOST /pvt/ordersmarketplaceOrderIdWhy 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 response and without mapping → STOP.
POST /pvt/ordersorderIdPOST .../oms/.../invoiceCorrect
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}POST /pvt/ordersorderIdmarketplaceOrderId重要性
在OMS URL中使用预留ID会导致无法匹配市场订单,发票和跟踪信息永远无法关联到客户订单。
检测规则
如果响应的和使用同一个变量且没有做映射 → 停止开发。
POST /pvt/ordersorderIdPOST .../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 to return a valid array for every request. When the request includes checkout context (e.g. , , ), the response MUST include aligned , for all relevant modes (including pickup when offered), and : true where required. The handler MUST complete within 2.5 seconds.
POST /pvt/orderForms/simulationitemspostalCodeclientProfileDatashippingDatalogisticsInfoslasallowMultipleDeliveriesWhy this matters
Incomplete or missing SLAs break checkout shipping selection. Slow responses mark offers unavailable and hurt conversion.
logisticsInfoDetection
If simulation returns only with prices but omits when the request had → warn. If p95 latency approaches 2s without caching → warn.
itemslogisticsInfoshippingDataCorrect
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 });卖家必须实现**接口,对每个请求返回有效的数组。当请求包含结账上下文(例如、、)时,响应必须包含对应的、所有相关配送模式的(如果提供自提则要包含),以及必要的。接口必须在2.5秒**内完成响应。
POST /pvt/orderForms/simulationitemspostalCodeclientProfileDatashippingDatalogisticsInfoslasallowMultipleDeliveries: true重要性
logisticsInfo检测规则
如果请求包含但模拟接口仅返回带价格的,缺失 → 告警。如果p95延迟在未做缓存的情况下接近2秒 → 告警。
shippingDataitemslogisticsInfo正确示例
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约束:下单接口必须返回卖家orderId
(预留ID)
orderIdPOST /pvt/ordersorderIdsellerOrderIdPOST /pvt/orders/{sellerOrderId}/fulfillWhy this matters
Omitting or reusing a fake breaks the link between marketplace order flow and your reservation and prevents dispatch from routing correctly.
orderIdDetection
If the handler returns 200 without adding , or returns a single object instead of an array → warn.
orderIdCorrect
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/ordersorderIdsellerOrderIdPOST /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 , , , , and array. The MUST be in cents. The array MUST match the items in the order.
typeinvoiceNumberinvoiceValueissuanceDateitemsinvoiceValueitemsWhy this matters
Missing required fields cause the API to reject the invoice with 400 Bad Request, leaving the order stuck in "handling" status. Incorrect (e.g., using dollars instead of cents) causes financial discrepancies in marketplace reconciliation.
invoiceValueDetection
If you see an invoice notification payload missing , , , or → warn about missing required fields. If appears to be in dollars (e.g., instead of ) → warn about cents conversion.
invoiceNumberinvoiceValueissuanceDateitemsinvoiceValue99.909990Correct
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
});
}发票通知必须包含、、、和数组。必须以分为单位。数组必须匹配订单中的商品。
typeinvoiceNumberinvoiceValueissuanceDateitemsinvoiceValueitems重要性
缺失必填字段会导致API返回400 Bad Request拒绝发票,订单会卡在“处理中”状态。不正确(例如使用美元而非分)会导致市场对账出现财务差异。
invoiceValue检测规则
如果发票通知请求体缺失、、或 → 告警缺失必填字段。如果看起来是美元格式(例如而非) → 告警需要转换为分单位。
invoiceNumberinvoiceValueissuanceDateitemsinvoiceValue99.909990正确示例
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 to add tracking to an existing invoice.
PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}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 MUST reflect only the items in that particular shipment.
invoiceValueWhy 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 . Parse ( = seller SKU, = seller id on marketplace). If the body has no / , treat as indexation: return availability (and minimal logistics if your contract requires it). If the body includes checkout fields, build per , populate (delivery + pickup-in-point with when applicable), , , set / , and : true. Keep CPU and I/O bounded so you stay under 2.5s.
POST /pvt/orderForms/simulationitems[]idsellerpostalCodeclientProfileDatalogisticsInfoitemIndexslaspickupStoreInfodeliveryChannelsstockBalancecountrypostalCodeallowMultipleDeliveriestypescript
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: [],
}));
}注册**接口。解析(=卖家SKU,=市场侧卖家ID)。如果请求体没有/,判定为索引构建场景:返回可用性信息(以及协议要求的最小logistics信息)。如果请求体包含结账字段,按构建,填充(配送+自提,包含如果适用)、、,设置/,以及。控制CPU和IO耗时,确保响应低于2.5秒**。
POST /pvt/orderForms/simulationitems[]idsellerpostalCodeclientProfileDataitemIndexlogisticsInfoslaspickupStoreInfodeliveryChannelsstockBalancecountrypostalCodeallowMultipleDeliveries: truetypescript
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 . Body is an array. Persist each order as a reservation; respond with the same objects plus (reservation key) and typically . That is what VTEX passes as on .
POST /pvt/ordersorderIdfollowUpEmailorderIdsellerOrderIdPOST /pvt/orders/{sellerOrderId}/fulfilltypescript
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);
};注册**接口。请求体是数组**。将每个订单持久化为预留记录;响应返回相同的对象,新增**(预留记录key)和通常的字段。该就是VTEX在中传入的**。
POST /pvt/ordersorderIdfollowUpEmailorderIdPOST /pvt/orders/{sellerOrderId}/fulfillsellerOrderIdtypescript
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-gradefor minimal calls can be unnecessary work; returning only prices for checkout calls without SLAs and
logisticsInfobreaks shipping selection.logisticsInfo -
Omittingon order placement. VTEX expects an array response echoing each order with
orderIdset to your reservation. Empty 200 responses or missingorderIdstrand the order pipeline.orderId -
Using reservationin OMS invoice URLs. After placement, you must use
orderIdfrom the protocol when callingmarketplaceOrderId. Confusing the two ids produces 404 or silent failure on invoice/tracking./api/oms/pvt/orders/... -
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 (). The Cancel Order API will reject the request.
type: "Input"
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.
- 将索引构建模拟等同于结账模拟。同一个接口在索引构建时接收极简请求体(无客户位置信息),在结账时接收完整请求体。对极简请求返回完整的是不必要的工作;对结账请求仅返回价格而缺失SLA和
logisticsInfo会破坏配送选择流程。logisticsInfo - 下单响应缺失。VTEX期望响应是数组,每个订单都设置
orderId为你方的预留ID。空的200响应或缺失orderId会导致订单流程中断。orderId - 在OMS发票URL中使用预留。下单后调用**
orderId时必须使用协议中返回的/api/oms/pvt/orders/...**。混淆两个ID会导致发票/跟踪请求返回404或静默失败。marketplaceOrderId - 履约授权前发送发票。卖家下单后立即发送发票通知,未收到市场的履约授权回调。此时支付可能仍在处理或审核中。授权前开票会导致发票被拒绝,订单状态不一致。仅在收到后再发送发票。
POST /pvt/orders/{sellerOrderId}/fulfill - 取消已开票订单未处理退货发票。卖家直接调用取消订单接口取消已开票订单,未先发送退货发票。订单处于“已开票”状态时,没有退货发票()无法取消。取消订单API会拒绝请求。
type: "Input"
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 handle both minimal (indexation) and checkout-shaped requests, returning
POST /pvt/orderForms/simulationandlogisticsInfowhen context is present?slas - Does accept an array and return each order with
POST /pvt/orders(reservation id)?orderId - Does read
POST /pvt/orders/{sellerOrderId}/fulfillandmarketplaceOrderIdand matchmarketplaceOrderGroupto the reservation from placement?sellerOrderId - Are OMS and
/invoicecalls usingPATCH .../invoice, not the reservation id?marketplaceOrderId - 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 in cents (not dollars)?
invoiceValue - 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 () first?
type: "Input" - Does the fulfillment simulation endpoint respond within 2.5 seconds?
- 是否同时处理极简(索引构建)和结账类型的请求,上下文存在时返回
POST /pvt/orderForms/simulation和logisticsInfo?slas - 是否接收数组,并为每个订单返回带
POST /pvt/orders(预留ID)的响应?orderId - 是否读取
POST /pvt/orders/{sellerOrderId}/fulfill和marketplaceOrderId,并将**marketplaceOrderGroup**与下单时的预留记录匹配?sellerOrderId - OMS **和
/invoice调用是否使用PATCH .../invoice**而非预留ID?marketplaceOrderId - 卖家是否仅在收到履约授权回调后才开始物理履约?
- 对于IO/BFF连接器:缓存和路由选择是否符合vtex-io技能要求(模拟SLA、数据范围)?
- 发票请求体是否包含所有必填字段(、
type、invoiceNumber、invoiceValue、issuanceDate)?items - 是否以分为单位(而非美元)?
invoiceValue - 跟踪信息是否在物流商提供真实数据后单独发送(而非硬编码占位符)?
- 拆分发货时,每张发票是否仅对应当前包裹的商品和金额?
- 取消已开票订单是否先发送退货发票()?
type: "Input" - 履约模拟接口是否在2.5秒内响应?
Reference
参考
- External Seller integration guide — End-to-end seller connector (fulfillment URL, orders, invoicing)
- Marketplace Protocol — External seller fulfillment — Simulation, order placement, authorize fulfillment, and related endpoints
- External Seller Connector — Order invoicing — When and how to notify invoices to the marketplace OMS
- Order Invoice Notification API — invoice to OMS (
POSTin path = marketplace order)orderId - Update Order Tracking API — tracking on an invoice
PATCH - Order Flow and Status — Order status lifecycle
VTEX also maintains an open reference implementation for the External Seller service under the GitHub repository (useful as a scaffold; align behavior with the official protocol docs above).
vtex-apps/external-seller-example- 外部卖家集成指南 —— 端到端卖家连接器(履约URL、订单、开票)
- 市场协议 —— 外部卖家履约 —— 模拟、下单、履约授权及相关接口
- 外部卖家连接器 —— 订单开票 —— 何时以及如何向市场OMS发送发票通知
- 订单发票通知API —— 向OMS发送发票POST请求(路径中的=市场订单ID)
orderId - 更新订单跟踪API —— PATCH请求更新发票的跟踪信息
- 订单流与状态 —— 订单状态生命周期
VTEX还在**GitHub仓库维护了外部卖家服务的开源参考实现**(可作为脚手架使用;请以上面的官方协议文档为准对齐行为)。
vtex-apps/external-seller-example